am 216c29ec: Reconcile with jb-release

* commit '216c29ec51de9616264a1e9ee3dd5ac840701d03':
diff --git a/java/proguard.flags b/java/proguard.flags
index 34e23aa..752ced3 100644
--- a/java/proguard.flags
+++ b/java/proguard.flags
@@ -41,13 +41,7 @@
 }
 
 -keep class com.android.inputmethod.latin.ResearchLogger {
-  void setLogFileManager(...);
-  void clearAll();
-  com.android.inputmethod.latin.ResearchLogger$LogFileManager getLogFileManager();
-}
-
--keep class com.android.inputmethod.latin.ResearchLogger$LogFileManager {
-  java.lang.String getContents();
+  void flush();
 }
 
 -keep class com.android.inputmethod.keyboard.KeyboardLayoutSet$Builder {
diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml
index 7431fce..22543ef 100644
--- a/java/res/values-af/strings.xml
+++ b/java/res/values-af/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-sleutelbord (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android-sleutelbordinstellings"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Invoeropsies"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Navorsing-loglêerbevele"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-speltoetser"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-speltoetser (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Speltoetser se instellings"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Steminvoer is gedeaktiveer"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Stel invoermetodes op"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Invoertale"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"let op die tydstempel in die loglêer"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Aangetekende tydstempel"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Moenie hierdie sessie aanteken nie"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Sessie se loglêer uitgevee"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Sessie se loglêer uitgevee"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Sessie se loglêer NIE uitgevee nie"</string>
     <string name="select_language" msgid="3693815588777926848">"Invoertale"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Raak weer om te stoor"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Woordeboek beskikbaar"</string>
diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml
index d70c05d..a1b168c 100644
--- a/java/res/values-am/strings.xml
+++ b/java/res/values-am/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"የAndroid ቁልፍ ሰሌዳ (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"የAndroid ቁልፍሰሌዳ ቅንብሮች"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"ግቤት አማራጮች"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"የጥናት የምዝግብ ማስታወሻ ትዕዛዞች"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android የፊደል ማረሚያ"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android የፊደል ማረሚያ (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"የፊደል አራሚ ቅንብሮች"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"የድምፅ ግቤት ቦዝኗል"</string>
     <string name="configure_input_method" msgid="373356270290742459">"ግቤት ሜተዶችን አዋቀር"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"ቋንቋዎች አግቤት"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"የምዝግብ ማስታወሻ ጊዜ ማህተም ማስታወሻ"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"የጊዜ ማህተም ተመዝግቧል"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"ይህን ክፍለ ጊዜ እንዳትመዘግበው"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"የክፍለጊዜ ምዝግብ ማስታወሻ በመሰረዝ ላይ"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"የክፍለ ጊዜ ምዝግብ ማስታወሻ ተሰርዟል"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"የክፍለጊዜ ምዝግብ ማስታወሻ አልተሰረዘም"</string>
     <string name="select_language" msgid="3693815588777926848">"ቋንቋዎች አግቤት"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"ለማስቀመጥ እንደገና ንካ"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"መዝገበ ቃላት አለ"</string>
diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml
index 5678c40..2a22074 100644
--- a/java/res/values-ar/strings.xml
+++ b/java/res/values-ar/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"لوحة مفاتيح Android ‏(AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"إعدادات لوحة مفاتيح Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"خيارات الإرسال"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"أوامر سجلات البحث"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"التدقيق الإملائي في Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"التدقيق الإملائي في Android‏ (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"إعدادات التدقيق الإملائي"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"الإدخال الصوتي مُعطل"</string>
     <string name="configure_input_method" msgid="373356270290742459">"تهيئة طرق الإدخال"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"لغات الإدخال"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"ملاحظة طابع زمني في سجل"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"تم تسجيل الطابع الزمني"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"عدم تسجيل هذه الجلسة"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"جارٍ حذف سجل الجلسة"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"تم حذف سجل الجلسة"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"لم يتم حذف سجل الجلسة"</string>
     <string name="select_language" msgid="3693815588777926848">"لغات الإدخال"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"المس مرة أخرى للحفظ"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"القاموس متاح"</string>
diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml
index 4d14565..ff3213a 100644
--- a/java/res/values-be/strings.xml
+++ b/java/res/values-be/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавіятура Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Налады клавіятуры Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Параметры ўводу"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Каманды гiсторыя даследаванняў"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Iнструмент праверкi правапiсу для Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Iнструмент праверкi правапiсу для Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Налады праверкі арфаграфіі"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Галасавы набор адкл."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Налада метадаў уводу"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Мовы ўводу"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Пазначыць час у гiсторыi"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Запiсаныя пазнакі"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Не рэгістраваць гэты сеанс"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Выдаленне гiсторыi сеанса"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Гiсторыя сеанса выдалена"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Гiсторыя сеанса НЕ выдалена"</string>
     <string name="select_language" msgid="3693815588777926848">"Мовы ўводу"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Дакраніцеся зноў, каб захаваць"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Слоўнік даступны"</string>
diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml
index 106a918..5e916ce 100644
--- a/java/res/values-bg/strings.xml
+++ b/java/res/values-bg/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавиатура на Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Настройки на клавиатурата на Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Опции за въвеждане"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Команди за рег. файл за проучвания"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Програма за правописна проверка за Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Програма за правописна проверка за Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Настройки за проверка на правописа"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Глас. въвежд. е деакт."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Конфигуриране на въвеждането"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Входни езици"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Отбелязване на часа в рег. файл"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Часът е записан"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Без регистр. на сесията"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Рег. файл на сесията се изтрива"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Рег. файл на сесията е изтрит"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Рег. файл на сесията НЕ Е изтрит"</string>
     <string name="select_language" msgid="3693815588777926848">"Езици за въвеждане"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Докоснете отново, за да запазите"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Има достъп до речник"</string>
diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml
index e3adbcf..1b7cfc2 100644
--- a/java/res/values-ca/strings.xml
+++ b/java/res/values-ca/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclat d\'Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Configuració del teclat d\'Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opcions d\'entrada"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Recerca d\'ordres de reg."</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Corrector ortogràfic d\'Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corrector ortogràfic d\'Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Configuració de la correcció ortogràfica"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Entr. veu desactiv."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configura mètodes d\'entrada"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Idiomes d\'entrada"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Indica m. horària al reg."</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Marca horària enregistrada"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"No enregistris la sessió"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Suprimint registre de ses."</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Registre de ses. suprimit"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Registre de ses. NO sup."</string>
     <string name="select_language" msgid="3693815588777926848">"Idiomes d\'entrada"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Torna a tocar per desar"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Diccionari disponible"</string>
diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml
index 90d840c..7e6ee0b 100644
--- a/java/res/values-cs/strings.xml
+++ b/java/res/values-cs/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Klávesnice Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Nastavení klávesnice Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Možnosti zadávání textu a dat"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Příkazy vývoj. protokolu"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Kontrola pravopisu Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Kontrola pravopisu Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Nastavení kontroly pravopisu"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Hlasový vstup vypnut"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurace metod zadávání"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Vstupní jazyky"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Uložit čas do protokolu"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Časové razítko vloženo"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Neprotokolovat relaci"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Mazání protokolu relace"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Protokol relace smazán"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Protokol relace nesmazán"</string>
     <string name="select_language" msgid="3693815588777926848">"Vstupní jazyky"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Opětovným dotykem provedete uložení"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Slovník k dispozici"</string>
diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml
index 50b0b0a..c6800a9 100644
--- a/java/res/values-da/strings.xml
+++ b/java/res/values-da/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-tastatur (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android-tastatur-indstillinger"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Indstillinger for input"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Forskningslogkommandoer"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-stavekontrol"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-stavekontrol (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Indstillinger for stavekontrol"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Stemmeinput deaktiveret"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurer inputmetoder"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Inputsprog"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Notér tidsstempel i log"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Noteret tidsstempel"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Logfør ikke denne session"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Sletter sessionslogfil"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Sessionslogfil slettet"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Sessionslog IKKE slettet"</string>
     <string name="select_language" msgid="3693815588777926848">"Inputsprog"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Tryk igen for at gemme"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Ordbog er tilgængelig"</string>
diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml
index 216aac5..cd2d78e 100644
--- a/java/res/values-de/strings.xml
+++ b/java/res/values-de/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-Tastatur (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android-Tastatureinstellungen"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Eingabeoptionen"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Forschungsprotokollbefehle"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-Rechtschreibprüfung"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-Rechtschreibprüfung (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Einstellungen für Rechtschreibprüfung"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Spracheingabe deaktiviert"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Eingabemethoden konfigurieren"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Eingabesprachen"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Zeitstempel im Protokoll"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Zeitstempel aufgenommen"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Nicht protokollieren"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Protokoll wird gelöscht..."</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Protokoll gelöscht"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Protokoll NICHT gelöscht"</string>
     <string name="select_language" msgid="3693815588777926848">"Eingabesprachen"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Zum Speichern erneut berühren"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Wörterbuch verfügbar"</string>
diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml
index 486346a..5dcbf43 100644
--- a/java/res/values-el/strings.xml
+++ b/java/res/values-el/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Πληκτρολόγιο Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Ρυθμίσεις πληκτρολογίου Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Επιλογές εισόδου"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Έρευνα εντολών καταγραφής"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Ορθογραφικός έλεγχος Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Ορθογραφικός έλεγχος Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Ρυθμίσεις ορθογραφικού ελέγχου"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Απεν. φωνητ. είσοδος"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Διαμόρφωση μεθόδων εισαγωγής"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Γλώσσες εισόδου"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Χρόνος στο αρχείο καταγρ."</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Καταγεγραμμένος χρόνος"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Χωρίς αρχείο καταγραφής"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Διαγραφή αρχείου σύνδεσης"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Αρχείο καταγρ. διαγράφηκε"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Αρχείο καταγρ. ΔΕΝ διαγρ."</string>
     <string name="select_language" msgid="3693815588777926848">"Γλώσσες εισόδου"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Αγγίξτε ξανά για αποθήκευση"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Λεξικό διαθέσιμο"</string>
diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml
index 5020076..cd9b218 100644
--- a/java/res/values-en-rGB/strings.xml
+++ b/java/res/values-en-rGB/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android keyboard (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android keyboard settings"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Input options"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Research Log Commands"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android spell checker"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android spell checker (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Spellchecking settings"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Voice input is disabled"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configure input methods"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Input languages"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Note timestamp in log"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Recorded timestamp"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Do not log this session"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Deleting session log"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Session log deleted"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Session log NOT deleted"</string>
     <string name="select_language" msgid="3693815588777926848">"Input languages"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Touch again to save"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dictionary available"</string>
diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml
index 34ad0a4..39ca409 100644
--- a/java/res/values-es-rUS/strings.xml
+++ b/java/res/values-es-rUS/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado de Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Configuración de teclado de Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opciones de entrada"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comandos registro invest."</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Corrector ortográfico de Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corrector ortográfico de Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Configuración del corrector ortográfico"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"La entrada por voz está inhabilitada"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configurar métodos de entrada"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Idiomas de entrada"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Marcar tiempo en registro"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Marca tiempo registrada"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"No registrar esta sesión"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Eliminando registro"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Registro sesión eliminado"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"NO se eliminó el registro"</string>
     <string name="select_language" msgid="3693815588777926848">"Idiomas de entrada"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Vuelve a tocar para guardar."</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Diccionario disponible"</string>
diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml
index 3c60aa6..79a36cc 100644
--- a/java/res/values-es/strings.xml
+++ b/java/res/values-es/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Ajustes del teclado de Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opciones entrada texto"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comandos registro investigación"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Corrector de Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corrector de Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Ajustes del corrector ortográfico"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Entrada de voz inhabilitada"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configurar métodos de entrada"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Idiomas"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Anotar marca tiempo en registro"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Marca de tiempo registrada"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"No registrar esta sesión"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Eliminando registro..."</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Registro eliminado"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Registro no eliminado"</string>
     <string name="select_language" msgid="3693815588777926848">"Idiomas de entrada"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Toca otra vez para guardar."</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Hay un diccionario disponible"</string>
diff --git a/java/res/values-et/strings.xml b/java/res/values-et/strings.xml
index 4a592c3..45870d3 100644
--- a/java/res/values-et/strings.xml
+++ b/java/res/values-et/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-klaviatuur (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Androidi klaviatuuriseaded"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Sisestusvalikud"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Uuringulogi käsud"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidi õigekirjakontroll"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidi õigekirjakontroll (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Õigekirjakontrolli seaded"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Kõnesisend on keelatud"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Sisestusmeetodite seadistamine"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Sisestuskeeled"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Märgi ajatempel logisse"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Salvestatud ajatemplid"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Ära logi seda seanssi"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Seansi logi kustutamine"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Seansi logi kustutatud"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Seansi logi EI kustutatud"</string>
     <string name="select_language" msgid="3693815588777926848">"Sisestuskeeled"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Salvestamiseks puudutage uuesti"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Sõnastik saadaval"</string>
diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml
index bc0e85b..9f0e5aa 100644
--- a/java/res/values-fa/strings.xml
+++ b/java/res/values-fa/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"صفحه کلید (Android (AOSP"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"تنظیمات صفحه کلید Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"گزینه های ورودی"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"فرمان‌های گزارش‌گیری پژوهش"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"غلط‌گیر املای Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"غلط‌گیر املای Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"تنظیمات غلط گیری املایی"</string>
@@ -115,6 +116,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"ورودی صدا غیرفعال است"</string>
     <string name="configure_input_method" msgid="373356270290742459">"پیکربندی روش های ورودی"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"زبان های ورودی"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"یادداشت مهر زمان در گزارش"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"مهر زمان ثبت شده"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"از این جلسه گزارش‌گیری نشود"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"در حال حذف گزارش جلسه"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"گزارش جلسه حذف شد"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"گزارش جلسه حذف نشد"</string>
     <string name="select_language" msgid="3693815588777926848">"زبان‌های ورودی"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"برای ذخیره دوباره لمس کنید"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"دیکشنری موجود است"</string>
diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml
index 97002ed..f4dff35 100644
--- a/java/res/values-fi/strings.xml
+++ b/java/res/values-fi/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-näppäimistö (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android-näppäimistön asetukset"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Syöttövalinnat"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Tutkimuslokin komennot"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-oikoluku"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-oikoluku (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Oikoluvun asetukset"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Ääniohjaus on pois käytöstä"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Määritä syöttötavat"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Syöttökielet"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Merkitse aikaleima lokiin"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Merkitty aikaleima"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Älä tallenna tätä käyttök."</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Poistetaan lokia"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Käyttökertaloki poistettu"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Lokia EI poistettu"</string>
     <string name="select_language" msgid="3693815588777926848">"Syöttökielet"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Tallenna koskettamalla uudelleen"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Sanakirja saatavilla"</string>
diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml
index 285d222..1094c4a 100644
--- a/java/res/values-fr/strings.xml
+++ b/java/res/values-fr/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Clavier Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Paramètres du clavier Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Options de saisie"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Commandes journaux rech."</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Correcteur orthographique Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Correcteur orthographique Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Paramètre du correcteur orthographique"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Saisie vocale désactivée"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configurer les modes de saisie"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Langues de saisie"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Noter heure dans journal"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Heure enregistrée."</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Ne pas enregistrer session"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Suppr. journal session…"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Journal session supprimé."</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Journal session PAS suppr."</string>
     <string name="select_language" msgid="3693815588777926848">"Langues de saisie"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Appuyer de nouveau pour enregistrer"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dictionnaire disponible"</string>
diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml
index 2b18084..c131e12 100644
--- a/java/res/values-hi/strings.xml
+++ b/java/res/values-hi/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android कीबोर्ड (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android कीबोर्ड सेटिंग"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"इनपुट विकल्‍प"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"लॉग आदेशों का शोध करें"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android वर्तनी परीक्षक"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android वर्तनी परीक्षक (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"वर्तनी जांच सेटिंग"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"ध्‍वनि इनपुट अक्षम है"</string>
     <string name="configure_input_method" msgid="373356270290742459">"इनपुट पद्धति कॉन्‍फ़िगर करें"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"इनपुट भाषा"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"लॉग में टाइमस्‍टैम्‍प नोट करें"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"रिकॉर्ड किया गया टाइमस्टैम्प"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"इस सत्र को लॉग न करें"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"सत्र लॉग हटाया जा रहा है"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"सत्र लॉग हटाया गया"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"सत्र लॉग हटाया नहीं गया"</string>
     <string name="select_language" msgid="3693815588777926848">"इनपुट भाषाएं"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"सहेजने के लिए पुन: स्‍पर्श करें"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"शब्‍दकोश उपलब्‍ध है"</string>
diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml
index a59d469..f8a198d 100644
--- a/java/res/values-hr/strings.xml
+++ b/java/res/values-hr/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android tipkovnica (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Postavke tipkovnice za Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opcije ulaza"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Istraživanje naredbi dnevnika"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidova provjera pravopisa"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidova provjera pravopisa (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Postavke provjere pravopisa"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Glas. unos onemog."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfiguriraj načine ulaza"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Jezici unosa"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Zabilježi razdoblje u dnevniku"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Zabilježeno razdoblje"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Ne bilježi ovu sesiju"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Brisanje dnevnika sesije"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Izbrisan dnevnik sesije"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Dnevnik sesije NIJE izbrisan"</string>
     <string name="select_language" msgid="3693815588777926848">"Jezici unosa"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Dodirnite ponovo za spremanje"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Rječnik je dostupan"</string>
diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml
index 0eac1a9..fc1ac12 100644
--- a/java/res/values-hu/strings.xml
+++ b/java/res/values-hu/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-billentyűzet (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android billentyűzetbeállítások"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Beviteli beállítások"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Naplózási parancsok"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidos helyesírás-ellenőrző"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidos helyesírás-ellenőrző (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Helyesírás-ellenőrzés beállításai"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Hangbevivel KI"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Beviteli módok beállítása"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Beviteli nyelvek"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Időbélyegző naplózáskor"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Rögzített időbélyegzők"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Ne naplózza"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Napló törlése folyamatban"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Napló törölve"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Napló NINCS törölve"</string>
     <string name="select_language" msgid="3693815588777926848">"Beviteli nyelvek"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Érintse meg újból a mentéshez"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Van elérhető szótár"</string>
diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml
index e0e92b5..a7a2c03 100644
--- a/java/res/values-in/strings.xml
+++ b/java/res/values-in/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Keyboard Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Setelan keyboard Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opsi masukan"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Riset Perintah Log"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Pemeriksa ejaan Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Pemeriksa ejaan Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Setelan pemeriksaan ejaan"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Masukan suara dinonaktifkan"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurasikan metode masukan"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Bahasa masukan"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Catat cap waktu di log"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Cap waktu yang direkam"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Jangan simpan log sesi ini"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Menghapus log sesi"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Log sesi dihapus"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Log sesi BELUM dihapus"</string>
     <string name="select_language" msgid="3693815588777926848">"Bahasa masukan"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Sentuh lagi untuk menyimpan"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Kamus yang tersedia"</string>
diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml
index a54958c..e77a0d0 100644
--- a/java/res/values-it/strings.xml
+++ b/java/res/values-it/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Tastiera Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Impostazioni tastiera Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opzioni inserimento"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Ricerca comandi di log"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Controllo ortografico Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Controllo ortografico Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Impostazioni di controllo ortografico"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Comandi vocali disatt."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configura metodi di immissione"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Lingue comandi"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Indicazione temporale log"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Indicazione temporale registrata"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Non registrare la sessione"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Eliminazione log sessione"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Log di sessione eliminato"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Log sessione non eliminato"</string>
     <string name="select_language" msgid="3693815588777926848">"Lingue comandi"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Tocca di nuovo per salvare"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dizionario disponibile"</string>
diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml
index 323cac1..000c9f3 100644
--- a/java/res/values-iw/strings.xml
+++ b/java/res/values-iw/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"מקלדת Android ‏(AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"הגדרות מקלדת של Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"אפשרויות קלט"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"פקודות יומן מחקר"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"בודק האיות של Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"בודק האיות של Android ‏(AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"הגדרות בדיקת איות"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"הקלט הקולי מושבת"</string>
     <string name="configure_input_method" msgid="373356270290742459">"הגדרת שיטות קלט"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"שפות קלט"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"ציין חותמת זמן ביומן"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"חותמת זמן מתועדת"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"אל תרשום הפעלה זו ביומן"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"מוחק יומן הפעלה"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"יומן הפעלה נמחק"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"יומן הפעלה לא נמחק"</string>
     <string name="select_language" msgid="3693815588777926848">"שפות קלט"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"גע שוב כדי לשמור"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"מילון זמין"</string>
diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml
index 3a86516..05dd3a3 100644
--- a/java/res/values-ja/strings.xml
+++ b/java/res/values-ja/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Androidキーボード(AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Androidキーボードの設定"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"入力オプション"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"ログコマンドの検索"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidスペルチェッカー"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidスペルチェッカー(AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"スペルチェックの設定"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"音声入力は無効です"</string>
     <string name="configure_input_method" msgid="373356270290742459">"入力方法を設定"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"入力言語"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"タイムスタンプを記録"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"タイムスタンプ記録済み"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"セッションを記録しない"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"セッションログ削除中"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"セッションログ削除済み"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"セッションログ未削除"</string>
     <string name="select_language" msgid="3693815588777926848">"入力言語"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"保存するにはもう一度タップ"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"辞書を利用できます"</string>
diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml
index 536f06d..6a99883 100644
--- a/java/res/values-ko/strings.xml
+++ b/java/res/values-ko/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android 키보드(AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android 키보드 설정"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"입력 옵션"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"로그 명령 탐색"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android 맞춤법 검사기"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android 맞춤법 검사기(AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"맞춤법 검사 설정"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"음성 입력이 사용 중지됨"</string>
     <string name="configure_input_method" msgid="373356270290742459">"입력 방법 설정"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"입력 언어"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"로그에 타임스탬프를 기록"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"타임스탬프를 기록함"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"이 세션을 로그하지 마세요."</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"세션 로그 삭제"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"세션 로그가 삭제됨"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"세션 로그가 삭제되지 않음"</string>
     <string name="select_language" msgid="3693815588777926848">"입력 언어"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"저장하려면 다시 터치"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"사전 사용 가능"</string>
diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml
index a0b8fc8..86bfc37 100644
--- a/java/res/values-lt/strings.xml
+++ b/java/res/values-lt/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"„Android“ klaviatūra (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"„Android“ klaviatūros nustatymai"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Įvesties parinktys"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Tyrinėti žurnalo komandas"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"„Android“ rašybos tikrinimo programa"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"„Android“ rašybos tikrinimo programa (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Rašybos tikrinimo nustatymai"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Balso įv. neleidž."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigūruoti įvesties metodus"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Įvesties kalbos"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Pažym. laiko žymę žurnale"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Įrašyta laiko žymė"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Neįrašyti šios sesijos"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Ištrinam. sesijos žurnal."</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Sesijos žurnalas ištrint."</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Sesij. žurnal. NEIŠTRINT."</string>
     <string name="select_language" msgid="3693815588777926848">"Įvesties kalbos"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Jei norite išsaugoti, palieskite dar kartą"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Žodynas galimas"</string>
diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml
index 93727a8..403e0f2 100644
--- a/java/res/values-lv/strings.xml
+++ b/java/res/values-lv/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android tastatūra (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android tastatūras iestatījumi"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Ievades opcijas"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Izpētes žurnāla komandas"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android pareizrakstības pārbaudītājs"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android pareizrakstības pārbaudītājs (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Pareizrakstības pārbaudes iestatījumi"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Balss iev. atspējota"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Ievades metožu konfigurēšana"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Ievades valodas"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Atzīmēt laiksp. žurnālā"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Laikspied. ir reģistrēts."</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Nereģistrēt šo sesiju"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Not. sesijas žurn. dzēš."</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Sesijas žurnāls ir dzēsts"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Sesijas žurn. NAV dzēsts"</string>
     <string name="select_language" msgid="3693815588777926848">"Ievades valodas"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Pieskarieties vēlreiz, lai saglabātu."</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Ir pieejama vārdnīca."</string>
diff --git a/java/res/values-ms/strings.xml b/java/res/values-ms/strings.xml
index a07c44f..60f5a41 100644
--- a/java/res/values-ms/strings.xml
+++ b/java/res/values-ms/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Papan kekunci Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Tetapan papan kekunci Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Pilihan input"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Arahan Log Penyelidikan"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Penyemak ejaan Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Penyemak ejaan Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Tetapan penyemakan ejaan"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Input suara dilmphkn"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurasikan kaedah input"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Bahasa input"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Tanda cap waktu dalam log"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Cap waktu direkodkan"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Jangan log sesi ini"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Memadam log sesi"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Log sesi dipadam"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Log sesi TIDAK dipadam"</string>
     <string name="select_language" msgid="3693815588777926848">"Bahasa input"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Sentuh lagi untuk menyimpan"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Kamus tersedia"</string>
diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml
index 8ed2376..fc2bd55 100644
--- a/java/res/values-nb/strings.xml
+++ b/java/res/values-nb/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-tastatur (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Innstillinger for skjermtastatur"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Inndataalternativer"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Kommandoer for undersøkelseslogging"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-stavekontroll"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-stavekontroll (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Innstillinger for stavekontroll"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Taleinndata er deaktiv."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurer inndatametoder"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Inndataspråk"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Notér tidsstempel i logg"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Registrerte tidsstempel"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Ikke loggfør denne økten"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Sletter øktloggen"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Øktloggen ble slettet"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Øktloggen ble IKKE slettet"</string>
     <string name="select_language" msgid="3693815588777926848">"Inndataspråk"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Trykk på nytt for å lagre"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Ordbok tilgjengelig"</string>
diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml
index a7d499b..26d5e3f 100644
--- a/java/res/values-nl/strings.xml
+++ b/java/res/values-nl/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-toetsenbord (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Instellingen voor Android-toetsenbord"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Invoeropties"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Opdrachten in onderzoekslogbestand"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Spellingcontrole van Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Spellingcontrole van Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Instellingen voor spellingcontrole"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Spraakinvoer is uit"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Invoermethoden configureren"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Invoertalen"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Tijdstempel opnemen in logbestand"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Opgenomen tijdstempel"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Sessie niet registreren"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Sessielogbestand verwijderen"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Sessielogbestand verwijderd"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Sessielogbestand NIET verwijderd"</string>
     <string name="select_language" msgid="3693815588777926848">"Invoertalen"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Raak nogmaals aan om op te slaan"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Woordenboek beschikbaar"</string>
diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml
index bf27782..dc4f396 100644
--- a/java/res/values-pl/strings.xml
+++ b/java/res/values-pl/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Klawiatura Androida (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Ustawienia klawiatury Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opcje wprowadzania"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Polecenia dziennika badań"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Słownik Androida"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Sprawdzanie pisowni na Androidzie (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Ustawienia sprawdzania pisowni"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Wprowadzanie głosowe jest wyłączone"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfiguruj metody wprowadzania"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Języki wprowadzania"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Znacznik czasu uwagi w dzienniku"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Zapisano znacznik czasu"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Nie rejestruj tej sesji"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Usuwanie dziennika sesji"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Usunięto dziennik sesji"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Dziennik sesji NIEUSUNIĘTY"</string>
     <string name="select_language" msgid="3693815588777926848">"Języki wprowadzania"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Dotknij ponownie, aby zapisać"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Słownik dostępny"</string>
diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml
index ccb042e..35eedd8 100644
--- a/java/res/values-pt-rPT/strings.xml
+++ b/java/res/values-pt-rPT/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Definições de teclado do Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opções de introdução"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comandos de Reg. Invest."</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Verificador ortográfico do Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Verificador ortográfico do Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Definições da verificação ortográfica"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Entr. voz desact."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configurar métodos de introdução"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Idiomas de entrada"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Anotar car. data no reg."</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Carimbo de data gravado"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Não registar esta sessão"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"A eliminar reg. da sessão"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Reg. de sessão eliminado"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Reg. de sessão NÃO elim."</string>
     <string name="select_language" msgid="3693815588777926848">"Idiomas de introdução"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Toque novamente para guardar"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dicionário disponível"</string>
diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml
index 7d64759..a12fc23 100644
--- a/java/res/values-pt/strings.xml
+++ b/java/res/values-pt/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Configurações de teclado Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opções de entrada"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Pesq. comandos de reg."</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Corretor ortográfico do Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corretor ortográfico do Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Configurações de verificação ortográfica"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Texto por voz desat."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configurar métodos de entrada"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Idiomas de entrada"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Indicar data/hora no reg."</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Data/hora registrada"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Não registrar esta sessão"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Excluindo reg. de sessão"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Registro excluído"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Registro NÃO excluído"</string>
     <string name="select_language" msgid="3693815588777926848">"Idiomas de entrada"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Toque novamente para salvar"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dicionário disponível"</string>
diff --git a/java/res/values-rm/strings.xml b/java/res/values-rm/strings.xml
index 18741f8..c6c936e 100644
--- a/java/res/values-rm/strings.xml
+++ b/java/res/values-rm/strings.xml
@@ -26,6 +26,8 @@
     <string name="english_ime_settings" msgid="6661589557206947774">"Parameters da la tastatura Android"</string>
     <!-- no translation found for english_ime_input_options (3909945612939668554) -->
     <skip />
+    <!-- no translation found for english_ime_research_log (8492602295696577851) -->
+    <skip />
     <!-- no translation found for spell_checker_service_name (7338064335159755926) -->
     <skip />
     <!-- no translation found for aosp_spell_checker_service_name (6985142605330377819) -->
@@ -190,6 +192,18 @@
     <!-- no translation found for configure_input_method (373356270290742459) -->
     <skip />
     <string name="language_selection_title" msgid="1651299598555326750">"Linguas da cumonds vocals"</string>
+    <!-- no translation found for note_timestamp_for_researchlog (1889446857977976026) -->
+    <skip />
+    <!-- no translation found for notify_recorded_timestamp (8036429032449612051) -->
+    <skip />
+    <!-- no translation found for do_not_log_this_session (413762473641146336) -->
+    <skip />
+    <!-- no translation found for notify_session_log_deleting (3299507647764414623) -->
+    <skip />
+    <!-- no translation found for notify_session_log_deleted (8687927130100934686) -->
+    <skip />
+    <!-- no translation found for notify_session_log_not_deleted (2592908998810755970) -->
+    <skip />
     <!-- no translation found for select_language (3693815588777926848) -->
     <skip />
     <!-- no translation found for hint_add_to_dictionary (573678656946085380) -->
diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml
index 0c4d8bc..523a855 100644
--- a/java/res/values-ro/strings.xml
+++ b/java/res/values-ro/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Tastatură Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Setările tastaturii Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opţiuni de introducere text"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comenzi jurnal cercetare"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Verificator ortografic Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Verificator ortografic Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Setări de verificare ortografică"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Intr. vocală dezact."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Configuraţi metodele de intrare"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Selectaţi limba"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Înreg. marc. temp. jurnal"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Marcaj temporal înregis."</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Nu înregistraţi sesiunea"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Se șterge jurnal sesiune"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Jurnal de sesiune șters"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Jurnal sesiune neşters"</string>
     <string name="select_language" msgid="3693815588777926848">"Limbi de intrare"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Atingeţi din nou pentru a salva"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dicţionar disponibil"</string>
diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml
index a9bd6ac..34fdb0f 100644
--- a/java/res/values-ru/strings.xml
+++ b/java/res/values-ru/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавиатура Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Клавиатура Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Настройки"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Все команды"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Проверка правописания Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Проверка правописания Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Настройка проверки правописания"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Голосовой ввод откл."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Настройка способов ввода"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Языки ввода"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Закладка в журнале"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Закладка сохранена"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Не сохранять этот сеанс"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Удаление…"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Запись сеанса удалена"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Запись сеанса НЕ удалена"</string>
     <string name="select_language" msgid="3693815588777926848">"Языки ввода"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Нажмите, чтобы сохранить"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Доступен словарь"</string>
diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml
index 44eecdc..41fc0cf 100644
--- a/java/res/values-sk/strings.xml
+++ b/java/res/values-sk/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Klávesnica Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Nastavenia klávesnice Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Možnosti zadávania textu a údajov"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Príkazy denníka výskumu"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Kontrola pravopisu Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Kontrola pravopisu Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Nastavenia kontroly pravopisu"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Hlasový vstup je zakázaný"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurovať metódy vstupu"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Jazyky vstupu"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Časová pečiatka denníka"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Časová pečiatka zaznamenaná"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Neukl. reláciu do denníka"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Odstraň. denníka relácie"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Denník relácie odstránený"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Denník relácie NIE JE odstr."</string>
     <string name="select_language" msgid="3693815588777926848">"Jazyky vstupu"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Opätovným dotykom uložíte"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"K dispozícii je slovník"</string>
diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml
index ce7dc70..f18de78 100644
--- a/java/res/values-sl/strings.xml
+++ b/java/res/values-sl/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Tipkovnica Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Nastavitve tipkovnice Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Možnosti vnosa"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Ukazi za dnevnik raziskav"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Črkovalnik za Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Črkovalnik za Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Nastavitve preverjanja črkovanja"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Glas. vnos je onem."</string>
     <string name="configure_input_method" msgid="373356270290742459">"Nastavitev načinov vnosa"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Jeziki vnosa"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"V dnev. zabeleži čas. žig"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Časovni žig zabeležen"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Brez dnevnika za to sejo"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Brisanje seje dnevnika"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Dnevnik seje izbrisan"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Dnevnik seje NI izbrisan"</string>
     <string name="select_language" msgid="3693815588777926848">"Jeziki vnosa"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Dotaknite se še enkrat, da shranite"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Slovar je na voljo"</string>
diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml
index 6d2899b..11e678e 100644
--- a/java/res/values-sr/strings.xml
+++ b/java/res/values-sr/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android тастатура (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Подешавања Android тастатуре"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Опције уноса"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Команде евиденције истраживања"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android провера правописа"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android провера правописа (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Подешавања провере правописа"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Гласовни унос је онемогућен"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Конфигурисање метода уноса"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Језици за унос"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Наведи временску ознаку у евиденцији"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Снимљена временска ознака"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Не евидентирај ову сесију"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Брисање евиденције сесије"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Евиденција сесије је обрисана"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Евиденција сесије НИЈЕ избрисана"</string>
     <string name="select_language" msgid="3693815588777926848">"Језици уноса"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Поново додирните да бисте сачували"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Речник је доступан"</string>
diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml
index 75e80d4..bf1e505 100644
--- a/java/res/values-sv/strings.xml
+++ b/java/res/values-sv/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Androids tangentbord (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Inställningar för Androids tangentbord"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Inmatningsalternativ"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Loggkommandon"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Stavningskontroll i Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Stavningskontroll i Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Inställningar för stavningskontroll"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Röstinmatning inaktiv"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurera inmatningsmetoder"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Inmatningsspråk"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Markera tidpunkt i loggen"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Tidpunkten har sparats"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Logga inte detta besök"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Besöksloggen tas bort"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Besöksloggen togs bort"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Besöksloggen togs EJ bort"</string>
     <string name="select_language" msgid="3693815588777926848">"Inmatningsspråk"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Spara genom att trycka igen"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"En ordlista är tillgänglig"</string>
diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml
index da51cb3..f416fcd 100644
--- a/java/res/values-sw/strings.xml
+++ b/java/res/values-sw/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Kicharazio cha Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Mipangilio ya kibodi ya Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Chaguo za uingizaji"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Amri za Kumbukumbu za Utafiti"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Kikagua tahajia cha Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Kikagua tahajia cha Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Mipangilio ya kukagua sarufi"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Uingizaji sauti umelemazwa"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Sanidi mbinu za uingizaji"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Lugha za uingizaji"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Dokeza mhuri wa muda kwenye kumbukumbu"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Mhuri wa muda uliorekodiwa"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Usihifadhi kumbukumbu za kipindi hiki"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Inafuta kumbukumbu za kipindi"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Kumbukumbu za kipindi zimefutwa"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Kumbukumbu za kipindi HAZIJAFUTWA"</string>
     <string name="select_language" msgid="3693815588777926848">"Lugha zinazoruhusiwa"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Gusa tena ili kuhifadhi"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Kamusi inapatikana"</string>
diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml
index da20d66..6f25a20 100644
--- a/java/res/values-th/strings.xml
+++ b/java/res/values-th/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android keyboard (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"การตั้งค่าแป้นพิมพ์ Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"ตัวเลือกการป้อนข้อมูล"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"คำสั่งบันทึกการวิจัย"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"แอนดรอยด์ตรวจสอบการสะกด"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"แอนดรอยด์ตรวจสอบการสะกด (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"การตั้งค่าการตรวจสอบการสะกด"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"ปิดใช้งานป้อนข้อมูลด้วยเสียง"</string>
     <string name="configure_input_method" msgid="373356270290742459">"กำหนดค่าวิธีการป้อนข้อมูล"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"ภาษาในการป้อนข้อมูล"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"จดเวลาบันทึกไว้ในบันทึก"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"บันทึกเวลาบันทึกแล้ว"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"อย่าบันทึกเซสชันนี้"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"กำลังลบบันทึกเซสชัน"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"ลบบันทึกเซสชันแล้ว"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"บันทึกเซสชันไม่ถูกลบ"</string>
     <string name="select_language" msgid="3693815588777926848">"ภาษาสำหรับการป้อนข้อมูล"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"แตะอีกครั้งเพื่อบัน​​ทึก"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"มีพจนานุกรมให้ใช้งาน"</string>
diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml
index 7d365b2..e2fe6c5 100644
--- a/java/res/values-tl/strings.xml
+++ b/java/res/values-tl/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android keyboard (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Mga setting ng Android keyboard"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Mga pagpipilian sa input"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Cmmnd sa Log ng Pnnliksik"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Pang-check ng pagbabaybay ng Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Pang-check ng pagbabaybay ng Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Mga setting ng pang-check ng pagbabaybay"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Hindi pinagana ang voice input"</string>
     <string name="configure_input_method" msgid="373356270290742459">"I-configure ang mga pamamaraan ng pag-input"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Mag-input ng mga wika"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Tandaan timestamp sa log"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Na-record na timestamp"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Huwag i-log ang session"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Tinatanggl log ng session"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Tinanggal log ng session"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"HND ntnggl log ng session"</string>
     <string name="select_language" msgid="3693815588777926848">"Mga wika ng input"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Pinduting muli upang i-save"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Available ang diksyunaryo"</string>
diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml
index b2f7391..16411b8 100644
--- a/java/res/values-tr/strings.xml
+++ b/java/res/values-tr/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android klavye (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android klavye ayarları"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Giriş seçenekleri"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Araştırma Günlüğü Komutları"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android yazım denetleyici"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android yazım denetleyici (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Yazım denetimi ayarları"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Sesle grş devre dışı"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Giriş yöntemlerini yapılandır"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Giriş dilleri"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Günlüğe zaman damgası koy"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Zaman damgası kaydedildi"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Bu oturumu günlüğe kaydetme"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Oturum günlüğü siliniyor"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Oturum günlüğü silindi"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Oturum günlüğü SİLİNMEDİ"</string>
     <string name="select_language" msgid="3693815588777926848">"Giriş dilleri"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Kaydetmek için tekrar dokunun"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Sözlük kullanılabilir"</string>
diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml
index e994e1a..c2e743b 100644
--- a/java/res/values-uk/strings.xml
+++ b/java/res/values-uk/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавіатура Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Налашт-ня клавіат. Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Парам. введення"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Команди журналу дослідж."</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Засіб перевірки орфографії Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Засіб перевірки орфографії Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Налаштування перевірки орфографії"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Голос. ввід вимкнено"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Налаштування методів введення"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Мови вводу"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Мітка часу в журналі"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Записана мітка часу"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Не реєструвати цю сесію"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Видалення журналу сесії"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Журнал сесії видалено"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Журнал сесії НЕ видалено"</string>
     <string name="select_language" msgid="3693815588777926848">"Мови введення"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Торкніться знову, щоб зберегти"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Словник доступний"</string>
diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml
index 753af18..9ce2d5a 100644
--- a/java/res/values-vi/strings.xml
+++ b/java/res/values-vi/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Bàn phím Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Cài đặt bàn phím Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Tùy chọn nhập"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Lệnh ghi nhật ký cho nghiên cứu"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Trình kiểm tra chính tả Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Trình kiểm tra chính tả Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Cài đặt kiểm tra chính tả"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Nhập liệu bằng giọng nói đã bị tắt"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Định cấu hình phương thức nhập"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Ngôn ngữ nhập"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Dấu thời gian ghi chú trong nhật ký"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Dấu thời gian đã ghi"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Không ghi nhật ký phiên này"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Đang xóa nhật ký phiên"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Đã xóa nhật ký phiên"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Nhật ký phiên KHÔNG bị xóa"</string>
     <string name="select_language" msgid="3693815588777926848">"Ngôn ngữ nhập"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Chạm lại để lưu"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Có sẵn từ điển"</string>
diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml
index efde541..4907f20 100644
--- a/java/res/values-zh-rCN/strings.xml
+++ b/java/res/values-zh-rCN/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android 键盘 (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android 键盘设置"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"输入选项"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"研究记录命令"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android 拼写检查工具"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android 拼写检查工具 (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"拼写检查设置"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"语音输入功能已停用"</string>
     <string name="configure_input_method" msgid="373356270290742459">"配置输入法"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"输入语言"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"标记记录中的时间"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"已标记时间"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"不记录本次会话"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"正在删除会话记录"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"会话记录已删除"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"未能删除会话记录"</string>
     <string name="select_language" msgid="3693815588777926848">"输入语言"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"再次触摸即可保存"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"有可用词典"</string>
diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml
index 51df022..9179e66 100644
--- a/java/res/values-zh-rTW/strings.xml
+++ b/java/res/values-zh-rTW/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android 鍵盤 (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android 鍵盤設定"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"輸入選項"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"研究紀錄指令"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android 拼字檢查"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android 拼字檢查 (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"拼字檢查設定"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"語音輸入已停用"</string>
     <string name="configure_input_method" msgid="373356270290742459">"設定輸入法"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"輸入語言"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"在紀錄中加註時間戳記"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"已記錄時間戳記"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"不要記錄這個工作階段"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"正在刪除工作階段紀錄"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"已刪除工作階段紀錄"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"「未」刪除工作階段紀錄"</string>
     <string name="select_language" msgid="3693815588777926848">"輸入語言"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"再次輕觸即可儲存"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"可使用字典"</string>
diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml
index d3f80e4..3f7c96b 100644
--- a/java/res/values-zu/strings.xml
+++ b/java/res/values-zu/strings.xml
@@ -24,6 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Ikhibhodi ye-Android (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Izilungiselelo zekhibhodi ye-Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Okukhethwa kukho kokungenayo"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Imiyalo yefayela lokungena lokucwaninga"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Isihloli sokupela se-Android"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Isihloli sokupela se-Android (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Izilungiselelo zokuhlola ukupela"</string>
@@ -111,6 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Okufakwayo ngezwi kuvinjelwe"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Misa izindlela zokufakwayo"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Izilimi zokufakwayo"</string>
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Qaphela isitembu sesikhathi efayeleni lokungena"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Isitembu sesikhathi esirekhodiwe"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Ungenzi ifayela lokungena lalesi sikhathi"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Isusa ifayela lokungena lesikhathi"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Ifayela lokungena lesikhathi lisusiwe"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Ifayela lokungena lesikhathi alisusiwe"</string>
     <string name="select_language" msgid="3693815588777926848">"Izilimi zokufakwayo"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Thinta futhi ukuze ulondoloze"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Isichazamazwi siyatholakala"</string>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 8d3319d..589830d 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -22,13 +22,10 @@
     <bool name="config_use_fullscreen_mode">false</bool>
     <bool name="config_enable_show_voice_key_option">true</bool>
     <bool name="config_enable_show_popup_on_keypress_option">true</bool>
-    <bool name="config_enable_next_word_suggestions_option">true</bool>
-    <bool name="config_enable_usability_study_mode_option">false</bool>
+    <!-- TODO: Disable the following configuration for production. -->
+    <bool name="config_enable_usability_study_mode_option">true</bool>
     <!-- Whether or not Popup on key press is enabled by default -->
     <bool name="config_default_popup_preview">true</bool>
-    <!-- Default value for next word suggestion: while showing suggestions for a word should we weigh
-         in the previous word? -->
-    <bool name="config_default_next_word_suggestions">true</bool>
     <!-- Default value for next word prediction: after entering a word and a space only, should we look
          at input history to suggest a hopefully helpful suggestions for the next word? -->
     <bool name="config_default_next_word_prediction">true</bool>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index d51d378..0b781af 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -26,6 +26,8 @@
     <string name="english_ime_settings">Android keyboard settings</string>
     <!-- Title for Latin keyboard input options dialog [CHAR LIMIT=25] -->
     <string name="english_ime_input_options">Input options</string>
+    <!-- Title for Latin keyboard research log dialog, which contains special commands for users that contribute data for research. [CHAR LIMIT=33] -->
+    <string name="english_ime_research_log">Research Log Commands</string>
 
     <!-- Name of Android spell checker service -->
     <string name="spell_checker_service_name">Android spell checker</string>
@@ -233,6 +235,20 @@
     <!-- Title for input language selection screen -->
     <string name="language_selection_title">Input languages</string>
 
+    <!-- Title for dialog option that lets user mark a particular time in the log for later review by experts [CHAR LIMIT=38] -->
+    <string name="note_timestamp_for_researchlog">Note timestamp in log</string>
+    <!-- Toast notification message that the time has been marked for later review. [CHAR LIMIT=25] -->
+    <string name="notify_recorded_timestamp">Recorded timestamp</string>
+
+    <!-- Title for dialog option to let users cancel logging and delete log for this session [CHAR LIMIT=35] -->
+    <string name="do_not_log_this_session">Do not log this session</string>
+    <!-- Toast notification that the system is processing the request to delete the log for this session [CHAR LIMIT=35] -->
+    <string name="notify_session_log_deleting">Deleting session log</string>
+    <!-- Toast notification that the system has successfully deleted the log for this session [CHAR LIMIT=35] -->
+    <string name="notify_session_log_deleted">Session log deleted</string>
+    <!-- Toast notification that the system has failed to delete the log for this session [CHAR LIMIT=35] -->
+    <string name="notify_session_log_not_deleted">Session log NOT deleted</string>
+
     <!-- Preference for input language selection -->
     <string name="select_language">Input languages</string>
 
diff --git a/java/res/xml/key_styles_common.xml b/java/res/xml/key_styles_common.xml
index 622da21..162119d 100644
--- a/java/res/xml/key_styles_common.xml
+++ b/java/res/xml/key_styles_common.xml
@@ -22,23 +22,8 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <!-- Base key style for the key which may have settings or tab key as popup key. -->
-    <switch>
-        <case
-            latin:clobberSettingsKey="true"
-        >
-            <key-style
-                latin:styleName="f1MoreKeysStyle"
-                latin:backgroundType="functional" />
-        </case>
-        <!-- clobberSettingsKey="false" -->
-        <default>
-            <key-style
-                latin:styleName="f1MoreKeysStyle"
-                latin:keyLabelFlags="hasPopupHint"
-                latin:moreKeys="!text/settings_as_more_key"
-                latin:backgroundType="functional" />
-        </default>
-    </switch>
+    <include
+        latin:keyboardLayout="@xml/key_styles_f1" />
     <!-- Functional key styles -->
     <switch>
         <case
diff --git a/java/res/xml/key_styles_f1.xml b/java/res/xml/key_styles_f1.xml
new file mode 100644
index 0000000..8dfc3cb
--- /dev/null
+++ b/java/res/xml/key_styles_f1.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, 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"
+>
+    <!-- Base key style for the key which may have settings or tab key as popup key. -->
+    <!-- Kept as a separate file for cleaner overriding by an overlay.  -->
+    <switch>
+        <case
+            latin:clobberSettingsKey="true"
+        >
+            <key-style
+                latin:styleName="f1MoreKeysStyle"
+                latin:backgroundType="functional" />
+        </case>
+        <!-- clobberSettingsKey="false" -->
+        <default>
+            <key-style
+                latin:styleName="f1MoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:moreKeys="!text/settings_as_more_key"
+                latin:backgroundType="functional" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml
index 1379819..d8bf784 100644
--- a/java/res/xml/prefs.xml
+++ b/java/res/xml/prefs.xml
@@ -115,23 +115,11 @@
                 android:persistent="true"
                 android:defaultValue="true" />
             <CheckBoxPreference
-                android:key="next_word_suggestion"
-                android:title="@string/bigram_suggestion"
-                android:summary="@string/bigram_suggestion_summary"
-                android:persistent="true"
-                android:defaultValue="true" />
-            <CheckBoxPreference
                 android:key="next_word_prediction"
                 android:title="@string/bigram_prediction"
                 android:summary="@string/bigram_prediction_summary"
                 android:persistent="true"
                 android:defaultValue="true" />
-            <CheckBoxPreference
-                android:key="enable_span_insert"
-                android:title="@string/enable_span_insert"
-                android:summary="@string/enable_span_insert_summary"
-                android:persistent="true"
-                android:defaultValue="true" />
             <PreferenceScreen
                 android:key="pref_vibration_duration_settings"
                 android:title="@string/prefs_keypress_vibration_duration_settings"/>
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 21f175d..6fc630d 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -89,7 +89,8 @@
     private static final int MINIMUM_LETTER_CODE = CODE_TAB;
 
     /** Special keys code. Must be negative.
-     * These should be aligned with values/keycodes.xml
+     * These should be aligned with KeyboardCodesSet.ID_TO_NAME[],
+     * KeyboardCodesSet.DEFAULT[] and KeyboardCodesSet.RTL[]
      */
     public static final int CODE_SHIFT = -1;
     public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
@@ -101,8 +102,9 @@
     public static final int CODE_ACTION_NEXT = -8;
     public static final int CODE_ACTION_PREVIOUS = -9;
     public static final int CODE_LANGUAGE_SWITCH = -10;
+    public static final int CODE_RESEARCH = -11;
     // Code value representing the code is not specified.
-    public static final int CODE_UNSPECIFIED = -11;
+    public static final int CODE_UNSPECIFIED = -12;
 
     public final KeyboardId mId;
     public final int mThemeId;
@@ -422,67 +424,67 @@
      * This class parses Keyboard XML file and eventually build a Keyboard.
      * The Keyboard XML file looks like:
      * <pre>
-     *   &gt;!-- xml/keyboard.xml --&lt;
-     *   &gt;Keyboard keyboard_attributes*&lt;
-     *     &gt;!-- Keyboard Content --&lt;
-     *     &gt;Row row_attributes*&lt;
-     *       &gt;!-- Row Content --&lt;
-     *       &gt;Key key_attributes* /&lt;
-     *       &gt;Spacer horizontalGap="32.0dp" /&lt;
-     *       &gt;include keyboardLayout="@xml/other_keys"&lt;
+     *   &lt;!-- xml/keyboard.xml --&gt;
+     *   &lt;Keyboard keyboard_attributes*&gt;
+     *     &lt;!-- Keyboard Content --&gt;
+     *     &lt;Row row_attributes*&gt;
+     *       &lt;!-- Row Content --&gt;
+     *       &lt;Key key_attributes* /&gt;
+     *       &lt;Spacer horizontalGap="32.0dp" /&gt;
+     *       &lt;include keyboardLayout="@xml/other_keys"&gt;
      *       ...
-     *     &gt;/Row&lt;
-     *     &gt;include keyboardLayout="@xml/other_rows"&lt;
+     *     &lt;/Row&gt;
+     *     &lt;include keyboardLayout="@xml/other_rows"&gt;
      *     ...
-     *   &gt;/Keyboard&lt;
+     *   &lt;/Keyboard&gt;
      * </pre>
-     * The XML file which is included in other file must have &gt;merge&lt; as root element,
+     * The XML file which is included in other file must have &lt;merge&gt; as root element,
      * such as:
      * <pre>
-     *   &gt;!-- xml/other_keys.xml --&lt;
-     *   &gt;merge&lt;
-     *     &gt;Key key_attributes* /&lt;
+     *   &lt;!-- xml/other_keys.xml --&gt;
+     *   &lt;merge&gt;
+     *     &lt;Key key_attributes* /&gt;
      *     ...
-     *   &gt;/merge&lt;
+     *   &lt;/merge&gt;
      * </pre>
      * and
      * <pre>
-     *   &gt;!-- xml/other_rows.xml --&lt;
-     *   &gt;merge&lt;
-     *     &gt;Row row_attributes*&lt;
-     *       &gt;Key key_attributes* /&lt;
-     *     &gt;/Row&lt;
+     *   &lt;!-- xml/other_rows.xml --&gt;
+     *   &lt;merge&gt;
+     *     &lt;Row row_attributes*&gt;
+     *       &lt;Key key_attributes* /&gt;
+     *     &lt;/Row&gt;
      *     ...
-     *   &gt;/merge&lt;
+     *   &lt;/merge&gt;
      * </pre>
      * You can also use switch-case-default tags to select Rows and Keys.
      * <pre>
-     *   &gt;switch&lt;
-     *     &gt;case case_attribute*&lt;
-     *       &gt;!-- Any valid tags at switch position --&lt;
-     *     &gt;/case&lt;
+     *   &lt;switch&gt;
+     *     &lt;case case_attribute*&gt;
+     *       &lt;!-- Any valid tags at switch position --&gt;
+     *     &lt;/case&gt;
      *     ...
-     *     &gt;default&lt;
-     *       &gt;!-- Any valid tags at switch position --&lt;
-     *     &gt;/default&lt;
-     *   &gt;/switch&lt;
+     *     &lt;default&gt;
+     *       &lt;!-- Any valid tags at switch position --&gt;
+     *     &lt;/default&gt;
+     *   &lt;/switch&gt;
      * </pre>
      * You can declare Key style and specify styles within Key tags.
      * <pre>
-     *     &gt;switch&lt;
-     *       &gt;case mode="email"&lt;
-     *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
+     *     &lt;switch&gt;
+     *       &lt;case mode="email"&gt;
+     *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
      *           keyLabel=".com"
-     *         /&lt;
-     *       &gt;/case&lt;
-     *       &gt;case mode="url"&lt;
-     *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
+     *         /&gt;
+     *       &lt;/case&gt;
+     *       &lt;case mode="url"&gt;
+     *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
      *           keyLabel="http://"
-     *         /&lt;
-     *       &gt;/case&lt;
-     *     &gt;/switch&lt;
+     *         /&gt;
+     *       &lt;/case&gt;
+     *     &lt;/switch&gt;
      *     ...
-     *     &gt;Key keyStyle="shift-key" ... /&lt;
+     *     &lt;Key keyStyle="shift-key" ... /&gt;
      * </pre>
      */
 
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 51a0f53..18e01fb 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -873,6 +873,7 @@
                 keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacer, 0, 0));
     }
 
+    @SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16
     @Override
     public void showKeyPreview(PointerTracker tracker) {
         if (!mShowKeyPreviewPopup) return;
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index babf6ec..34e428e 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -242,10 +242,6 @@
                     + " ignoreModifier=" + ignoreModifierKey
                     + " enabled=" + key.isEnabled());
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(key,
-                    ignoreModifierKey);
-        }
         if (ignoreModifierKey) {
             return false;
         }
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 1207c3f..1bc8254 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -27,6 +27,8 @@
 import java.util.HashMap;
 
 public class ProximityInfo {
+    /** MAX_PROXIMITY_CHARS_SIZE must be the same as MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+     * in defines.h */
     public static final int MAX_PROXIMITY_CHARS_SIZE = 16;
     /** Number of key widths from current touch point to search for nearest keys. */
     private static float SEARCH_DISTANCE = 1.2f;
@@ -75,27 +77,6 @@
         mNativeProximityInfo = createNativeProximityInfo();
     }
 
-    // TODO: Remove this public constructor when the native part of the ProximityInfo becomes
-    // immutable.
-    // This public constructor aims only for test purpose.
-    public ProximityInfo(ProximityInfo o) {
-        mLocaleStr = o.mLocaleStr;
-        mGridWidth = o.mGridWidth;
-        mGridHeight = o.mGridHeight;
-        mGridSize = o.mGridSize;
-        mCellWidth = o.mCellWidth;
-        mCellHeight = o.mCellHeight;
-        mKeyboardMinWidth = o.mKeyboardMinWidth;
-        mKeyboardHeight = o.mKeyboardHeight;
-        mKeyHeight = o.mKeyHeight;
-        mMostCommonKeyWidth = o.mMostCommonKeyWidth;
-        mKeys = o.mKeys;
-        mTouchPositionCorrection = o.mTouchPositionCorrection;
-        mGridNeighbors = new Key[mGridSize][];
-        computeNearestNeighbors();
-        mNativeProximityInfo = createNativeProximityInfo();
-    }
-
     public static ProximityInfo createDummyProximityInfo() {
         return new ProximityInfo("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index 67cb74f..f7981a3 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -52,6 +52,7 @@
         "key_action_next",
         "key_action_previous",
         "key_language_switch",
+        "key_research",
         "key_unspecified",
         "key_left_parenthesis",
         "key_right_parenthesis",
@@ -86,6 +87,7 @@
         Keyboard.CODE_ACTION_NEXT,
         Keyboard.CODE_ACTION_PREVIOUS,
         Keyboard.CODE_LANGUAGE_SWITCH,
+        Keyboard.CODE_RESEARCH,
         Keyboard.CODE_UNSPECIFIED,
         CODE_LEFT_PARENTHESIS,
         CODE_RIGHT_PARENTHESIS,
@@ -112,6 +114,7 @@
         DEFAULT[11],
         DEFAULT[12],
         DEFAULT[13],
+        DEFAULT[14],
         CODE_RIGHT_PARENTHESIS,
         CODE_LEFT_PARENTHESIS,
         CODE_GREATER_THAN_SIGN,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 43ffb85..4ab6832 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -21,8 +21,6 @@
 
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.ResearchLogger;
-import com.android.inputmethod.latin.define.ProductionFlag;
 
 /**
  * Keyboard state machine.
@@ -305,9 +303,6 @@
             Log.d(TAG, "onPressKey: code=" + Keyboard.printableCode(code)
                    + " single=" + isSinglePointer + " autoCaps=" + autoCaps + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onPressKey(code, this);
-        }
         if (code == Keyboard.CODE_SHIFT) {
             onPressShift();
         } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
@@ -341,9 +336,6 @@
             Log.d(TAG, "onReleaseKey: code=" + Keyboard.printableCode(code)
                     + " sliding=" + withSliding + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onReleaseKey(this, code, withSliding);
-        }
         if (code == Keyboard.CODE_SHIFT) {
             onReleaseShift(withSliding);
         } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
@@ -375,9 +367,6 @@
         if (DEBUG_EVENT) {
             Log.d(TAG, "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onLongPressTimeout(code, this);
-        }
         if (mIsAlphabetMode && code == Keyboard.CODE_SHIFT) {
             mLongPressShiftLockFired = true;
             mSwitchActions.hapticAndAudioFeedback(code);
@@ -509,9 +498,6 @@
         if (DEBUG_EVENT) {
             Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onCancelInput(isSinglePointer, this);
-        }
         // Switch back to the previous keyboard mode if the user cancels sliding input.
         if (isSinglePointer) {
             if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
@@ -543,9 +529,6 @@
                     + " single=" + isSinglePointer
                     + " autoCaps=" + autoCaps + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onCodeInput(code, isSinglePointer, autoCaps, this);
-        }
 
         switch (mSwitchState) {
         case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 34308df..10e511e 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -52,6 +52,9 @@
     /** The number of contacts in the most recent dictionary rebuild. */
     static private int sContactCountAtLastRebuild = 0;
 
+    /** The locale for this contacts dictionary. Controls name bigram predictions. */
+    public final Locale mLocale;
+
     private ContentObserver mObserver;
 
     /**
@@ -61,6 +64,7 @@
 
     public ContactsBinaryDictionary(final Context context, final int dicTypeId, Locale locale) {
         super(context, getFilenameWithLocale(NAME, locale.toString()), dicTypeId);
+        mLocale = locale;
         mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
         registerObserver(context);
 
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
deleted file mode 100644
index cbfbd0e..0000000
--- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2009 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.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.keyboard.Keyboard;
-
-// TODO: This class is superseded by {@link ContactsBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words from Contacts provider.
- *
- * @deprecated Use {@link ContactsBinaryDictionary}.
- */
-@Deprecated
-public class ContactsDictionary extends ExpandableDictionary {
-
-    private static final String[] PROJECTION = {
-        BaseColumns._ID,
-        Contacts.DISPLAY_NAME,
-    };
-
-    private static final String TAG = "ContactsDictionary";
-
-    /**
-     * Frequency for contacts information into the dictionary
-     */
-    private static final int FREQUENCY_FOR_CONTACTS = 40;
-    private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
-
-    private static final int INDEX_NAME = 1;
-
-    private ContentObserver mObserver;
-
-    private long mLastLoadedContacts;
-
-    public ContactsDictionary(final Context context, final int dicTypeId) {
-        super(context, dicTypeId);
-        registerObserver(context);
-        loadDictionary();
-    }
-
-    private synchronized void registerObserver(final Context context) {
-        // Perform a managed query. The Activity will handle closing and requerying the cursor
-        // when needed.
-        if (mObserver != null) return;
-        ContentResolver cres = context.getContentResolver();
-        cres.registerContentObserver(
-                Contacts.CONTENT_URI, true, mObserver = new ContentObserver(null) {
-                    @Override
-                    public void onChange(boolean self) {
-                        setRequiresReload(true);
-                    }
-                });
-    }
-
-    public void reopen(final Context context) {
-        registerObserver(context);
-    }
-
-    @Override
-    public synchronized void close() {
-        if (mObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-            mObserver = null;
-        }
-        super.close();
-    }
-
-    @Override
-    public void startDictionaryLoadingTaskLocked() {
-        long now = SystemClock.uptimeMillis();
-        if (mLastLoadedContacts == 0
-                || now - mLastLoadedContacts > 30 * 60 * 1000 /* 30 minutes */) {
-            super.startDictionaryLoadingTaskLocked();
-        }
-    }
-
-    @Override
-    public void loadDictionaryAsync() {
-        try {
-            Cursor cursor = getContext().getContentResolver()
-                    .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
-            if (cursor != null) {
-                addWords(cursor);
-            }
-        } catch(IllegalStateException e) {
-            Log.e(TAG, "Contacts DB is having problems");
-        }
-        mLastLoadedContacts = SystemClock.uptimeMillis();
-    }
-
-    @Override
-    public void getBigrams(final WordComposer codes, final CharSequence previousWord,
-            final WordCallback callback) {
-        // Do not return bigrams from Contacts when nothing was typed.
-        if (codes.size() <= 0) return;
-        super.getBigrams(codes, previousWord, callback);
-    }
-
-    private void addWords(Cursor cursor) {
-        clearDictionary();
-
-        final int maxWordLength = getMaxWordLength();
-        try {
-            if (cursor.moveToFirst()) {
-                while (!cursor.isAfterLast()) {
-                    String name = cursor.getString(INDEX_NAME);
-
-                    if (name != null && -1 == name.indexOf('@')) {
-                        int len = name.length();
-                        String prevWord = null;
-
-                        // TODO: Better tokenization for non-Latin writing systems
-                        for (int i = 0; i < len; i++) {
-                            if (Character.isLetter(name.charAt(i))) {
-                                int j;
-                                for (j = i + 1; j < len; j++) {
-                                    char c = name.charAt(j);
-
-                                    if (!(c == Keyboard.CODE_DASH
-                                            || c == Keyboard.CODE_SINGLE_QUOTE
-                                            || Character.isLetter(c))) {
-                                        break;
-                                    }
-                                }
-
-                                String word = name.substring(i, j);
-                                i = j - 1;
-
-                                // Safeguard against adding really long words. Stack
-                                // may overflow due to recursion
-                                // Also don't add single letter words, possibly confuses
-                                // capitalization of i.
-                                final int wordLen = word.length();
-                                if (wordLen < maxWordLength && wordLen > 1) {
-                                    super.addWord(word, null /* shortcut */,
-                                            FREQUENCY_FOR_CONTACTS);
-                                    if (!TextUtils.isEmpty(prevWord)) {
-                                        super.setBigramAndGetFrequency(prevWord, word,
-                                                FREQUENCY_FOR_CONTACTS_BIGRAM);
-                                    }
-                                    prevWord = word;
-                                }
-                            }
-                        }
-                    }
-                    cursor.moveToNext();
-                }
-            }
-            cursor.close();
-        } catch(IllegalStateException e) {
-            Log.e(TAG, "Contacts DB is having problems");
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 7cd9bc2..9c3d46e 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -115,4 +115,12 @@
     public void close() {
         // empty base implementation
     }
+
+    /**
+     * Subclasses may override to indicate that this Dictionary is not yet properly initialized.
+     */
+
+    public boolean isInitialized() {
+        return true;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 1a05fcd..26c2e63 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -82,8 +82,9 @@
         return maxFreq;
     }
 
-    public boolean isEmpty() {
-        return mDictionaries.isEmpty();
+    @Override
+    public boolean isInitialized() {
+        return !mDictionaries.isEmpty();
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
deleted file mode 100644
index 0f34d50..0000000
--- a/java/src/com/android/inputmethod/latin/EditingUtils.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-
-import java.util.regex.Pattern;
-
-/**
- * Utility methods to deal with editing text through an InputConnection.
- */
-public class EditingUtils {
-    /**
-     * Number of characters we want to look back in order to identify the previous word
-     */
-    // Provision for a long word pair and a separator
-    private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
-    private static final int INVALID_CURSOR_POSITION = -1;
-
-    private EditingUtils() {
-        // Unintentional empty constructor for singleton.
-    }
-
-    private static int getCursorPosition(InputConnection connection) {
-        if (null == connection) return INVALID_CURSOR_POSITION;
-        final ExtractedText extracted = connection.getExtractedText(new ExtractedTextRequest(), 0);
-        if (extracted == null) {
-            return INVALID_CURSOR_POSITION;
-        }
-        return extracted.startOffset + extracted.selectionStart;
-    }
-
-    /**
-     * @param connection connection to the current text field.
-     * @param separators characters which may separate words
-     * @return the word that surrounds the cursor, including up to one trailing
-     *   separator. For example, if the field contains "he|llo world", where |
-     *   represents the cursor, then "hello " will be returned.
-     */
-    public static String getWordAtCursor(InputConnection connection, String separators) {
-        // getWordRangeAtCursor returns null if the connection is null
-        Range r = getWordRangeAtCursor(connection, separators);
-        return (r == null) ? null : r.mWord;
-    }
-
-    /**
-     * Represents a range of text, relative to the current cursor position.
-     */
-    public static class Range {
-        /** Characters before selection start */
-        public final int mCharsBefore;
-
-        /**
-         * Characters after selection start, including one trailing word
-         * separator.
-         */
-        public final int mCharsAfter;
-
-        /** The actual characters that make up a word */
-        public final String mWord;
-
-        public Range(int charsBefore, int charsAfter, String word) {
-            if (charsBefore < 0 || charsAfter < 0) {
-                throw new IndexOutOfBoundsException();
-            }
-            this.mCharsBefore = charsBefore;
-            this.mCharsAfter = charsAfter;
-            this.mWord = word;
-        }
-    }
-
-    private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
-        if (connection == null || sep == null) {
-            return null;
-        }
-        CharSequence before = connection.getTextBeforeCursor(1000, 0);
-        CharSequence after = connection.getTextAfterCursor(1000, 0);
-        if (before == null || after == null) {
-            return null;
-        }
-
-        // Find first word separator before the cursor
-        int start = before.length();
-        while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
-
-        // Find last word separator after the cursor
-        int end = -1;
-        while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
-            // Nothing to do here.
-        }
-
-        int cursor = getCursorPosition(connection);
-        if (start >= 0 && cursor + end <= after.length() + before.length()) {
-            String word = before.toString().substring(start, before.length())
-                    + after.toString().substring(0, end);
-            return new Range(before.length() - start, end, word);
-        }
-
-        return null;
-    }
-
-    private static boolean isWhitespace(int code, String whitespace) {
-        return whitespace.contains(String.valueOf((char) code));
-    }
-
-    private static final Pattern spaceRegex = Pattern.compile("\\s+");
-
-    public static CharSequence getPreviousWord(InputConnection connection,
-            String sentenceSeperators) {
-        //TODO: Should fix this. This could be slow!
-        if (null == connection) return null;
-        CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
-        return getPreviousWord(prev, sentenceSeperators);
-    }
-
-    // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
-    // Also, it won't return words that end in a separator.
-    // Example :
-    // "abc def|" -> abc
-    // "abc def |" -> abc
-    // "abc def. |" -> abc
-    // "abc def . |" -> def
-    // "abc|" -> null
-    // "abc |" -> null
-    // "abc. def|" -> null
-    public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
-        if (prev == null) return null;
-        String[] w = spaceRegex.split(prev);
-
-        // If we can't find two words, or we found an empty word, return null.
-        if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
-
-        // If ends in a separator, return null
-        char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
-        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
-        return w[w.length - 2];
-    }
-
-    public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
-        if (null == connection) return null;
-        final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
-        return getThisWord(prev, sentenceSeperators);
-    }
-
-    // Get the word immediately before the cursor, even if there is whitespace between it and
-    // the cursor - but not if there is punctuation.
-    // Example :
-    // "abc def|" -> def
-    // "abc def |" -> def
-    // "abc def. |" -> null
-    // "abc def . |" -> null
-    public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
-        if (prev == null) return null;
-        String[] w = spaceRegex.split(prev);
-
-        // No word : return null
-        if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
-
-        // If ends in a separator, return null
-        char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
-        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
-        return w[w.length - 1];
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index 34a92fd..4a5471c 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -514,8 +514,10 @@
 
     /**
      * Adds bigrams to the in-memory trie structure that is being used to retrieve any word
+     * @param word1 the first word of this bigram
+     * @param word2 the second word of this bigram
      * @param frequency frequency for this bigram
-     * @param addFrequency if true, it adds to current frequency, else it overwrites the old value
+     * @param fcp an instance of ForgettingCurveParams to use for decay policy
      * @return returns the final bigram frequency
      */
     private int setBigramAndGetFrequency(
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 97e898a..8a5fc49 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -56,7 +56,6 @@
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.accessibility.AccessibilityUtils;
@@ -103,27 +102,6 @@
      */
     private static final String SCHEME_PACKAGE = "package";
 
-    /** Whether to use the binary version of the contacts dictionary */
-    public static final boolean USE_BINARY_CONTACTS_DICTIONARY = true;
-
-    /** Whether to use the binary version of the user dictionary */
-    public static final boolean USE_BINARY_USER_DICTIONARY = true;
-
-    // TODO: migrate this to SettingsValues
-    private int mSuggestionVisibility;
-    private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
-            = R.string.prefs_suggestion_visibility_show_value;
-    private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
-            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
-    private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
-            = R.string.prefs_suggestion_visibility_hide_value;
-
-    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
-        SUGGESTION_VISIBILILTY_SHOW_VALUE,
-        SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
-        SUGGESTION_VISIBILILTY_HIDE_VALUE
-    };
-
     private static final int SPACE_STATE_NONE = 0;
     // Double space: the state where the user pressed space twice quickly, which LatinIME
     // resolved as period-space. Undoing this converts the period to a space.
@@ -143,8 +121,7 @@
     // Current space state of the input method. This can be any of the above constants.
     private int mSpaceState;
 
-    private SettingsValues mSettingsValues;
-    private InputAttributes mInputAttributes;
+    private SettingsValues mCurrentSettings;
 
     private View mExtractArea;
     private View mKeyPreviewBackingView;
@@ -162,15 +139,13 @@
     private boolean mShouldSwitchToLastSubtype = true;
 
     private boolean mIsMainDictionaryAvailable;
-    // TODO: revert this back to the concrete class after transition.
-    private Dictionary mUserDictionary;
+    private UserBinaryDictionary mUserDictionary;
     private UserHistoryDictionary mUserHistoryDictionary;
     private boolean mIsUserDictionaryAvailable;
 
     private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
     private WordComposer mWordComposer = new WordComposer();
-
-    private int mCorrectionMode;
+    private RichInputConnection mConnection = new RichInputConnection();
 
     // Keep track of the last selection range to decide if we need to show word alternatives
     private static final int NOT_A_CURSOR_POSITION = -1;
@@ -203,7 +178,6 @@
 
     public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
         private static final int MSG_UPDATE_SHIFT_STATE = 1;
-        private static final int MSG_SPACE_TYPED = 4;
         private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
         private static final int MSG_PENDING_IMS_CALLBACK = 6;
         private static final int MSG_UPDATE_SUGGESTIONS = 7;
@@ -211,6 +185,7 @@
         private int mDelayUpdateSuggestions;
         private int mDelayUpdateShiftState;
         private long mDoubleSpacesTurnIntoPeriodTimeout;
+        private long mDoubleSpaceTimerStart;
 
         public UIHandler(LatinIME outerInstance) {
             super(outerInstance);
@@ -275,16 +250,16 @@
         }
 
         public void startDoubleSpacesTimer() {
-            removeMessages(MSG_SPACE_TYPED);
-            sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout);
+            mDoubleSpaceTimerStart = SystemClock.uptimeMillis();
         }
 
         public void cancelDoubleSpacesTimer() {
-            removeMessages(MSG_SPACE_TYPED);
+            mDoubleSpaceTimerStart = 0;
         }
 
         public boolean isAcceptingDoubleSpaces() {
-            return hasMessages(MSG_SPACE_TYPED);
+            return SystemClock.uptimeMillis() - mDoubleSpaceTimerStart
+                    < mDoubleSpacesTurnIntoPeriodTimeout;
         }
 
         // Working variables for the following methods.
@@ -393,7 +368,7 @@
         mPrefs = prefs;
         LatinImeLogger.init(this, prefs);
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.init(this, prefs);
+            ResearchLogger.getInstance().init(this, prefs);
         }
         InputMethodManagerCompatWrapper.init(this);
         SubtypeSwitcher.init(this);
@@ -411,14 +386,12 @@
 
         loadSettings();
 
-        ImfUtils.setAdditionalInputMethodSubtypes(this, mSettingsValues.getAdditionalSubtypes());
-
-        // TODO: remove the following when it's not needed by updateCorrectionMode() any more
-        mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
-        updateCorrectionMode();
+        ImfUtils.setAdditionalInputMethodSubtypes(this, mCurrentSettings.getAdditionalSubtypes());
 
         Utils.GCUtils.getInstance().reset();
         boolean tryGC = true;
+        // Shouldn't this be removed? I think that from Honeycomb on, the GC is now actually working
+        // as expected and this code is useless.
         for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
             try {
                 initSuggest();
@@ -454,14 +427,16 @@
         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
         // is not guaranteed. It may even be called at the same time on a different thread.
         if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        final InputAttributes inputAttributes =
+                new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
         final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
             @Override
             protected SettingsValues job(Resources res) {
-                return new SettingsValues(mPrefs, LatinIME.this);
+                return new SettingsValues(mPrefs, inputAttributes, LatinIME.this);
             }
         };
-        mSettingsValues = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
-        mFeedbackManager = new AudioAndHapticFeedbackManager(this, mSettingsValues);
+        mCurrentSettings = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
+        mFeedbackManager = new AudioAndHapticFeedbackManager(this, mCurrentSettings);
         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
     }
 
@@ -469,7 +444,7 @@
         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
         final String localeStr = subtypeLocale.toString();
 
-        final Dictionary oldContactsDictionary;
+        final ContactsBinaryDictionary oldContactsDictionary;
         if (mSuggest != null) {
             oldContactsDictionary = mSuggest.getContactsDictionary();
             mSuggest.close();
@@ -477,19 +452,17 @@
             oldContactsDictionary = null;
         }
         mSuggest = new Suggest(this, subtypeLocale);
-        if (mSettingsValues.mAutoCorrectEnabled) {
-            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+        if (mCurrentSettings.mCorrectionEnabled) {
+            mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
         }
 
         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
-
-        if (USE_BINARY_USER_DICTIONARY) {
-            mUserDictionary = new UserBinaryDictionary(this, localeStr);
-            mIsUserDictionaryAvailable = ((UserBinaryDictionary)mUserDictionary).isEnabled();
-        } else {
-            mUserDictionary = new UserDictionary(this, localeStr);
-            mIsUserDictionaryAvailable = ((UserDictionary)mUserDictionary).isEnabled();
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().initSuggest(mSuggest);
         }
+
+        mUserDictionary = new UserBinaryDictionary(this, localeStr);
+        mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
         mSuggest.setUserDictionary(mUserDictionary);
 
         resetContactsDictionary(oldContactsDictionary);
@@ -505,36 +478,37 @@
     /**
      * Resets the contacts dictionary in mSuggest according to the user settings.
      *
-     * This method takes an optional contacts dictionary to use. Since the contacts dictionary
-     * does not depend on the locale, it can be reused across different instances of Suggest.
-     * The dictionary will also be opened or closed as necessary depending on the settings.
+     * This method takes an optional contacts dictionary to use when the locale hasn't changed
+     * since the contacts dictionary can be opened or closed as necessary depending on the settings.
      *
      * @param oldContactsDictionary an optional dictionary to use, or null
      */
-    private void resetContactsDictionary(final Dictionary oldContactsDictionary) {
-        final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
+    private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
+        final boolean shouldSetDictionary = (null != mSuggest && mCurrentSettings.mUseContactsDict);
 
-        final Dictionary dictionaryToUse;
+        final ContactsBinaryDictionary dictionaryToUse;
         if (!shouldSetDictionary) {
             // Make sure the dictionary is closed. If it is already closed, this is a no-op,
             // so it's safe to call it anyways.
             if (null != oldContactsDictionary) oldContactsDictionary.close();
             dictionaryToUse = null;
-        } else if (null != oldContactsDictionary) {
-            // Make sure the old contacts dictionary is opened. If it is already open, this is a
-            // no-op, so it's safe to call it anyways.
-            if (USE_BINARY_CONTACTS_DICTIONARY) {
-                ((ContactsBinaryDictionary)oldContactsDictionary).reopen(this);
-            } else {
-                ((ContactsDictionary)oldContactsDictionary).reopen(this);
-            }
-            dictionaryToUse = oldContactsDictionary;
         } else {
-            if (USE_BINARY_CONTACTS_DICTIONARY) {
-                dictionaryToUse = new ContactsBinaryDictionary(this, Suggest.DIC_CONTACTS,
-                        mSubtypeSwitcher.getCurrentSubtypeLocale());
+            final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+            if (null != oldContactsDictionary) {
+                if (!oldContactsDictionary.mLocale.equals(locale)) {
+                    // If the locale has changed then recreate the contacts dictionary. This
+                    // allows locale dependent rules for handling bigram name predictions.
+                    oldContactsDictionary.close();
+                    dictionaryToUse = new ContactsBinaryDictionary(
+                        this, Suggest.DIC_CONTACTS, locale);
+                } else {
+                    // Make sure the old contacts dictionary is opened. If it is already open,
+                    // this is a no-op, so it's safe to call it anyways.
+                    oldContactsDictionary.reopen(this);
+                    dictionaryToUse = oldContactsDictionary;
+                }
             } else {
-                dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
+                dictionaryToUse = new ContactsBinaryDictionary(this, Suggest.DIC_CONTACTS, locale);
             }
         }
 
@@ -569,9 +543,10 @@
         if (mDisplayOrientation != conf.orientation) {
             mDisplayOrientation = conf.orientation;
             mHandler.startOrientationChanging();
-            final InputConnection ic = getCurrentInputConnection();
-            commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
-            if (ic != null) ic.finishComposingText(); // For voice input
+            mConnection.beginBatchEdit(getCurrentInputConnection());
+            commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+            mConnection.finishComposingText();
+            mConnection.endBatchEdit();
             if (isShowingOptionDialog())
                 mOptionsDialog.dismiss();
         }
@@ -660,6 +635,7 @@
                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().start();
             ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, mPrefs);
         }
         if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
@@ -699,7 +675,6 @@
         updateFullscreenMode();
         mLastSelectionStart = editorInfo.initialSelStart;
         mLastSelectionEnd = editorInfo.initialSelEnd;
-        mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
         mApplicationSpecifiedCompletions = null;
 
         inputView.closing();
@@ -709,14 +684,12 @@
         mSpaceState = SPACE_STATE_NONE;
 
         loadSettings();
-        updateCorrectionMode();
-        updateSuggestionVisibility(mResources);
 
-        if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
-            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+        if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) {
+            mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
         }
 
-        switcher.loadKeyboard(editorInfo, mSettingsValues);
+        switcher.loadKeyboard(editorInfo, mCurrentSettings);
 
         if (mSuggestionsView != null)
             mSuggestionsView.clear();
@@ -726,19 +699,24 @@
         mHandler.postUpdateSuggestions();
         mHandler.cancelDoubleSpacesTimer();
 
-        inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
-                mSettingsValues.mKeyPreviewPopupDismissDelay);
+        inputView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
+                mCurrentSettings.mKeyPreviewPopupDismissDelay);
         inputView.setProximityCorrectionEnabled(true);
 
         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
     }
 
+    @Override
     public void onTargetApplicationKnown(final ApplicationInfo info) {
         mTargetApplicationInfo = info;
     }
 
     @Override
     public void onWindowHidden() {
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd,
+                    getCurrentInputConnection());
+        }
         super.onWindowHidden();
         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
         if (inputView != null) inputView.closing();
@@ -748,6 +726,9 @@
         super.onFinishInput();
 
         LatinImeLogger.commit();
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().stop();
+        }
 
         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
         if (inputView != null) inputView.closing();
@@ -768,7 +749,6 @@
             int composingSpanStart, int composingSpanEnd) {
         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
                 composingSpanStart, composingSpanEnd);
-
         if (DEBUG) {
             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
                     + ", ose=" + oldSelEnd
@@ -780,9 +760,16 @@
                     + ", ce=" + composingSpanEnd);
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
+            final boolean expectingUpdateSelectionFromLogger =
+                    ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
             ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
-                    composingSpanEnd);
+                    composingSpanEnd, mExpectingUpdateSelection,
+                    expectingUpdateSelectionFromLogger, mConnection);
+            if (expectingUpdateSelectionFromLogger) {
+                // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
+                return;
+            }
         }
 
         // TODO: refactor the following code to be less contrived.
@@ -841,7 +828,7 @@
      */
     @Override
     public void onExtractedTextClicked() {
-        if (isSuggestionsRequested()) return;
+        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
 
         super.onExtractedTextClicked();
     }
@@ -857,7 +844,7 @@
      */
     @Override
     public void onExtractedCursorMovement(int dx, int dy) {
-        if (isSuggestionsRequested()) return;
+        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
 
         super.onExtractedCursorMovement(dx, dy);
     }
@@ -888,33 +875,32 @@
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
         }
-        if (mInputAttributes.mApplicationSpecifiedCompletionOn) {
-            mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
-            if (applicationSpecifiedCompletions == null) {
-                clearSuggestions();
-                return;
-            }
-
-            final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
-                    SuggestedWords.getFromApplicationSpecifiedCompletions(
-                            applicationSpecifiedCompletions);
-            final SuggestedWords suggestedWords = new SuggestedWords(
-                    applicationSuggestedWords,
-                    false /* typedWordValid */,
-                    false /* hasAutoCorrectionCandidate */,
-                    false /* allowsToBeAutoCorrected */,
-                    false /* isPunctuationSuggestions */,
-                    false /* isObsoleteSuggestions */,
-                    false /* isPrediction */);
-            // When in fullscreen mode, show completions generated by the application
-            final boolean isAutoCorrection = false;
-            setSuggestions(suggestedWords, isAutoCorrection);
-            setAutoCorrectionIndicator(isAutoCorrection);
-            // TODO: is this the right thing to do? What should we auto-correct to in
-            // this case? This says to keep whatever the user typed.
-            mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
-            setSuggestionStripShown(true);
+        if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
+        mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+        if (applicationSpecifiedCompletions == null) {
+            clearSuggestions();
+            return;
         }
+
+        final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
+                SuggestedWords.getFromApplicationSpecifiedCompletions(
+                        applicationSpecifiedCompletions);
+        final SuggestedWords suggestedWords = new SuggestedWords(
+                applicationSuggestedWords,
+                false /* typedWordValid */,
+                false /* hasAutoCorrectionCandidate */,
+                false /* allowsToBeAutoCorrected */,
+                false /* isPunctuationSuggestions */,
+                false /* isObsoleteSuggestions */,
+                false /* isPrediction */);
+        // When in fullscreen mode, show completions generated by the application
+        final boolean isAutoCorrection = false;
+        setSuggestions(suggestedWords, isAutoCorrection);
+        setAutoCorrectionIndicator(isAutoCorrection);
+        // TODO: is this the right thing to do? What should we auto-correct to in
+        // this case? This says to keep whatever the user typed.
+        mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
+        setSuggestionStripShown(true);
     }
 
     private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
@@ -1001,7 +987,7 @@
     public boolean onEvaluateFullscreenMode() {
         // Reread resource value here, because this method is called by framework anytime as needed.
         final boolean isFullscreenModeAllowed =
-                mSettingsValues.isFullscreenModeAllowed(getResources());
+                mCurrentSettings.isFullscreenModeAllowed(getResources());
         return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
     }
 
@@ -1016,15 +1002,11 @@
     }
 
     // This will reset the whole input state to the starting state. It will clear
-    // the composing word, reset the last composed word, tell the inputconnection
-    // and the composingStateManager about it.
+    // the composing word, reset the last composed word, tell the inputconnection about it.
     private void resetEntireInputState() {
         resetComposingState(true /* alsoResetLastComposedWord */);
         updateSuggestions();
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            ic.finishComposingText();
-        }
+        mConnection.finishComposingText();
     }
 
     private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1033,15 +1015,13 @@
             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
     }
 
-    public void commitTyped(final InputConnection ic, final int separatorCode) {
+    public void commitTyped(final int separatorCode) {
         if (!mWordComposer.isComposingWord()) return;
         final CharSequence typedWord = mWordComposer.getTypedWord();
         if (typedWord.length() > 0) {
-            if (ic != null) {
-                ic.commitText(typedWord, 1);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_commitText(typedWord);
-                }
+            mConnection.commitText(typedWord, 1);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.latinIME_commitText(typedWord);
             }
             final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
             mLastComposedWord = mWordComposer.commitWord(
@@ -1052,7 +1032,7 @@
     }
 
     public int getCurrentAutoCapsState() {
-        if (!mSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+        if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
 
         final EditorInfo ei = getCurrentInputEditorInfo();
         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
@@ -1070,27 +1050,23 @@
         // unless needed.
         if (mWordComposer.isComposingWord()) return Constants.TextUtils.CAP_MODE_OFF;
 
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return Constants.TextUtils.CAP_MODE_OFF;
         // TODO: This blocking IPC call is heavy. Consider doing this without using IPC calls.
         // Note: getCursorCapsMode() returns the current capitalization mode that is any
         // combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none
         // of them.
-        return ic.getCursorCapsMode(inputType);
+        return mConnection.getCursorCapsMode(inputType);
     }
 
-    // "ic" may be null
-    private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
-        if (null == ic) return;
-        CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
+    private void swapSwapperAndSpace() {
+        CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
         // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
         if (lastTwo != null && lastTwo.length() == 2
                 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
-            ic.deleteSurroundingText(2, 0);
+            mConnection.deleteSurroundingText(2, 0);
             if (ProductionFlag.IS_EXPERIMENTAL) {
                 ResearchLogger.latinIME_deleteSurroundingText(2);
             }
-            ic.commitText(lastTwo.charAt(1) + " ", 1);
+            mConnection.commitText(lastTwo.charAt(1) + " ", 1);
             if (ProductionFlag.IS_EXPERIMENTAL) {
                 ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
             }
@@ -1098,18 +1074,17 @@
         }
     }
 
-    private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
-        if (mCorrectionMode == Suggest.CORRECTION_NONE) return false;
-        if (ic == null) return false;
-        final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
+    private boolean maybeDoubleSpace() {
+        if (!mCurrentSettings.mCorrectionEnabled) return false;
+        if (!mHandler.isAcceptingDoubleSpaces()) return false;
+        final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
         if (lastThree != null && lastThree.length() == 3
                 && canBeFollowedByPeriod(lastThree.charAt(0))
                 && lastThree.charAt(1) == Keyboard.CODE_SPACE
-                && lastThree.charAt(2) == Keyboard.CODE_SPACE
-                && mHandler.isAcceptingDoubleSpaces()) {
+                && lastThree.charAt(2) == Keyboard.CODE_SPACE) {
             mHandler.cancelDoubleSpacesTimer();
-            ic.deleteSurroundingText(2, 0);
-            ic.commitText(". ", 1);
+            mConnection.deleteSurroundingText(2, 0);
+            mConnection.commitText(". ", 1);
             if (ProductionFlag.IS_EXPERIMENTAL) {
                 ResearchLogger.latinIME_doubleSpaceAutoPeriod();
             }
@@ -1131,26 +1106,9 @@
                 || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
     }
 
-    // "ic" may be null
-    private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
-        if (ic == null) return;
-        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
-        if (lastOne != null && lastOne.length() == 1
-                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
-            ic.deleteSurroundingText(1, 0);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_deleteSurroundingText(1);
-            }
-        }
-    }
-
     @Override
     public boolean addWordToDictionary(String word) {
-        if (USE_BINARY_USER_DICTIONARY) {
-            ((UserBinaryDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
-        } else {
-            ((UserDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
-        }
+        mUserDictionary.addWordToUserDictionary(word, 128);
         // Suggestion strip should be updated after the operation of adding word to the
         // user dictionary
         mHandler.postUpdateSuggestions();
@@ -1193,17 +1151,14 @@
     }
 
     private void performEditorAction(int actionId) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            ic.performEditorAction(actionId);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_performEditorAction(actionId);
-            }
+        mConnection.performEditorAction(actionId);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_performEditorAction(actionId);
         }
     }
 
     private void handleLanguageSwitchKey() {
-        final boolean includesOtherImes = mSettingsValues.mIncludesOtherImesInLanguageSwitchList;
+        final boolean includesOtherImes = mCurrentSettings.mIncludesOtherImesInLanguageSwitchList;
         final IBinder token = getWindow().getWindow().getAttributes().token;
         if (mShouldSwitchToLastSubtype) {
             final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
@@ -1221,12 +1176,12 @@
         }
     }
 
-    static private void sendUpDownEnterOrBackspace(final int code, final InputConnection ic) {
+    private void sendUpDownEnterOrBackspace(final int code) {
         final long eventTime = SystemClock.uptimeMillis();
-        ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
+        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
                 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
-        ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
                 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
     }
@@ -1239,24 +1194,21 @@
             return;
         }
 
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
-            // we want to be able to compile against the Ice Cream Sandwich SDK.
-            if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
-                    && mTargetApplicationInfo.targetSdkVersion < 16) {
-                // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
-                // a hardware keyboard event on pressing enter or delete. This is bad for many
-                // reasons (there are race conditions with commits) but some applications are
-                // relying on this behavior so we continue to support it for older apps.
-                sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER, ic);
-            } else {
-                final String text = new String(new int[] { code }, 0, 1);
-                ic.commitText(text, text.length());
-            }
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_sendKeyCodePoint(code);
-            }
+        // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
+        // we want to be able to compile against the Ice Cream Sandwich SDK.
+        if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
+                && mTargetApplicationInfo.targetSdkVersion < 16) {
+            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
+            // a hardware keyboard event on pressing enter or delete. This is bad for many
+            // reasons (there are race conditions with commits) but some applications are
+            // relying on this behavior so we continue to support it for older apps.
+            sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER);
+        } else {
+            final String text = new String(new int[] { code }, 0, 1);
+            mConnection.commitText(text, text.length());
+        }
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_sendKeyCodePoint(code);
         }
     }
 
@@ -1268,11 +1220,10 @@
             mDeleteCount = 0;
         }
         mLastKeyTime = when;
+        mConnection.beginBatchEdit(getCurrentInputConnection());
 
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            if (ResearchLogger.sIsLogging) {
-                ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y);
-            }
+            ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
         }
 
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
@@ -1321,14 +1272,18 @@
         case Keyboard.CODE_LANGUAGE_SWITCH:
             handleLanguageSwitchKey();
             break;
+        case Keyboard.CODE_RESEARCH:
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.getInstance().presentResearchDialog(this);
+            }
+            break;
         default:
-            if (primaryCode == Keyboard.CODE_TAB
-                    && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
+            if (primaryCode == Keyboard.CODE_TAB && mCurrentSettings.isEditorActionNext()) {
                 performEditorAction(EditorInfo.IME_ACTION_NEXT);
                 break;
             }
             mSpaceState = SPACE_STATE_NONE;
-            if (mSettingsValues.isWordSeparator(primaryCode)) {
+            if (mCurrentSettings.isWordSeparator(primaryCode)) {
                 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
             } else {
                 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
@@ -1349,23 +1304,22 @@
                 && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
             mLastComposedWord.deactivate();
         mEnteredText = null;
+        mConnection.endBatchEdit();
     }
 
     @Override
     public void onTextInput(CharSequence text) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return;
-        ic.beginBatchEdit();
-        commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
-        text = specificTldProcessingOnTextInput(ic, text);
+        mConnection.beginBatchEdit(getCurrentInputConnection());
+        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+        text = specificTldProcessingOnTextInput(text);
         if (SPACE_STATE_PHANTOM == mSpaceState) {
             sendKeyCodePoint(Keyboard.CODE_SPACE);
         }
-        ic.commitText(text, 1);
+        mConnection.commitText(text, 1);
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_commitText(text);
         }
-        ic.endBatchEdit();
+        mConnection.endBatchEdit();
         mKeyboardSwitcher.updateShiftState();
         mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
         mSpaceState = SPACE_STATE_NONE;
@@ -1373,9 +1327,7 @@
         resetComposingState(true /* alsoResetLastComposedWord */);
     }
 
-    // ic may not be null
-    private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
-            final CharSequence text) {
+    private CharSequence specificTldProcessingOnTextInput(final CharSequence text) {
         if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
                 || !Character.isLetter(text.charAt(1))) {
             // Not a tld: do nothing.
@@ -1384,7 +1336,7 @@
         // We have a TLD (or something that looks like this): make sure we don't add
         // a space even if currently in phantom mode.
         mSpaceState = SPACE_STATE_NONE;
-        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
+        final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
         if (lastOne != null && lastOne.length() == 1
                 && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
             return text.subSequence(1, text.length());
@@ -1400,24 +1352,15 @@
     }
 
     private void handleBackspace(final int spaceState) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return;
-        ic.beginBatchEdit();
-        handleBackspaceWhileInBatchEdit(spaceState, ic);
-        ic.endBatchEdit();
-    }
-
-    // "ic" may not be null.
-    private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
         // In many cases, we may have to put the keyboard in auto-shift state again.
         mHandler.postUpdateShiftState();
 
-        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
+        if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
             // Cancel multi-character input: remove the text we just entered.
             // This is triggered on backspace after a key that inputs multiple characters,
             // like the smiley key or the .com key.
             final int length = mEnteredText.length();
-            ic.deleteSurroundingText(length, 0);
+            mConnection.deleteSurroundingText(length, 0);
             if (ProductionFlag.IS_EXPERIMENTAL) {
                 ResearchLogger.latinIME_deleteSurroundingText(length);
             }
@@ -1431,7 +1374,7 @@
             final int length = mWordComposer.size();
             if (length > 0) {
                 mWordComposer.deleteLast();
-                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+                mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
                 // If we have deleted the last remaining character of a word, then we are not
                 // isComposingWord() any more.
                 if (!mWordComposer.isComposingWord()) {
@@ -1442,7 +1385,7 @@
                     mHandler.postUpdateSuggestions();
                 }
             } else {
-                ic.deleteSurroundingText(1, 0);
+                mConnection.deleteSurroundingText(1, 0);
                 if (ProductionFlag.IS_EXPERIMENTAL) {
                     ResearchLogger.latinIME_deleteSurroundingText(1);
                 }
@@ -1450,17 +1393,18 @@
         } else {
             if (mLastComposedWord.canRevertCommit()) {
                 Utils.Stats.onAutoCorrectionCancellation();
-                revertCommit(ic);
+                revertCommit();
                 return;
             }
             if (SPACE_STATE_DOUBLE == spaceState) {
-                if (revertDoubleSpaceWhileInBatchEdit(ic)) {
+                mHandler.cancelDoubleSpacesTimer();
+                if (mConnection.revertDoubleSpace()) {
                     // No need to reset mSpaceState, it has already be done (that's why we
                     // receive it as a parameter)
                     return;
                 }
             } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
-                if (revertSwapPunctuation(ic)) {
+                if (mConnection.revertSwapPunctuation()) {
                     // Likewise
                     return;
                 }
@@ -1471,8 +1415,8 @@
             if (mLastSelectionStart != mLastSelectionEnd) {
                 // If there is a selection, remove it.
                 final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
-                ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
-                ic.deleteSurroundingText(lengthToDelete, 0);
+                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+                mConnection.deleteSurroundingText(lengthToDelete, 0);
                 if (ProductionFlag.IS_EXPERIMENTAL) {
                     ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
                 }
@@ -1490,40 +1434,39 @@
                     // a hardware keyboard event on pressing enter or delete. This is bad for many
                     // reasons (there are race conditions with commits) but some applications are
                     // relying on this behavior so we continue to support it for older apps.
-                    sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL, ic);
+                    sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL);
                 } else {
-                    ic.deleteSurroundingText(1, 0);
+                    mConnection.deleteSurroundingText(1, 0);
                 }
                 if (ProductionFlag.IS_EXPERIMENTAL) {
                     ResearchLogger.latinIME_deleteSurroundingText(1);
                 }
                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
-                    ic.deleteSurroundingText(1, 0);
+                    mConnection.deleteSurroundingText(1, 0);
                     if (ProductionFlag.IS_EXPERIMENTAL) {
                         ResearchLogger.latinIME_deleteSurroundingText(1);
                     }
                 }
             }
-            if (isSuggestionsRequested()) {
-                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
+            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
             }
         }
     }
 
-    // ic may be null
-    private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
+    private boolean maybeStripSpace(final int code,
             final int spaceState, final boolean isFromSuggestionStrip) {
         if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
-            removeTrailingSpaceWhileInBatchEdit(ic);
+            mConnection.removeTrailingSpace();
             return false;
         } else if ((SPACE_STATE_WEAK == spaceState
                 || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
                 && isFromSuggestionStrip) {
-            if (mSettingsValues.isWeakSpaceSwapper(code)) {
+            if (mCurrentSettings.isWeakSpaceSwapper(code)) {
                 return true;
             } else {
-                if (mSettingsValues.isWeakSpaceStripper(code)) {
-                    removeTrailingSpaceWhileInBatchEdit(ic);
+                if (mCurrentSettings.isWeakSpaceStripper(code)) {
+                    mConnection.removeTrailingSpace();
                 }
                 return false;
             }
@@ -1534,20 +1477,10 @@
 
     private void handleCharacter(final int primaryCode, final int x,
             final int y, final int spaceState) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (null != ic) ic.beginBatchEdit();
-        // TODO: if ic is null, does it make any sense to call this?
-        handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
-        if (null != ic) ic.endBatchEdit();
-    }
-
-    // "ic" may be null without this crashing, but the behavior will be really strange
-    private void handleCharacterWhileInBatchEdit(final int primaryCode,
-            final int x, final int y, final int spaceState, final InputConnection ic) {
         boolean isComposingWord = mWordComposer.isComposingWord();
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
+                !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) {
             if (isComposingWord) {
                 // Sanity check
                 throw new RuntimeException("Should not be composing here");
@@ -1559,8 +1492,9 @@
         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
         // thread here.
         if (!isComposingWord && (isAlphabet(primaryCode)
-                || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
-                && isSuggestionsRequested() && !isCursorTouchingWord()) {
+                || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode))
+                && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) &&
+                !mConnection.isCursorTouchingWord(mCurrentSettings)) {
             // Reset entirely the composing state anyway, then start composing a new word unless
             // the character is a single quote. The idea here is, single quote is not a
             // separator and it should be treated as a normal character, except in the first
@@ -1576,23 +1510,21 @@
         if (isComposingWord) {
             mWordComposer.add(
                     primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
-            if (ic != null) {
-                // If it's the first letter, make note of auto-caps state
-                if (mWordComposer.size() == 1) {
-                    mWordComposer.setAutoCapitalized(
-                            getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
-                }
-                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+            // If it's the first letter, make note of auto-caps state
+            if (mWordComposer.size() == 1) {
+                mWordComposer.setAutoCapitalized(
+                        getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
             }
+            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
             mHandler.postUpdateSuggestions();
         } else {
-            final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
+            final boolean swapWeakSpace = maybeStripSpace(primaryCode,
                     spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
 
             sendKeyCodePoint(primaryCode);
 
             if (swapWeakSpace) {
-                swapSwapperAndSpaceWhileInBatchEdit(ic);
+                swapSwapperAndSpace();
                 mSpaceState = SPACE_STATE_WEAK;
             }
             // Some characters are not word separators, yet they don't start a new
@@ -1619,37 +1551,31 @@
 
         boolean didAutoCorrect = false;
         // Handle separator
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            ic.beginBatchEdit();
-        }
         if (mWordComposer.isComposingWord()) {
             // In certain languages where single quote is a separator, it's better
             // not to auto correct, but accept the typed word. For instance,
             // in Italian dov' should not be expanded to dove' because the elision
             // requires the last vowel to be removed.
-            final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
-                    && !mInputAttributes.mInputTypeNoAutoCorrect;
-            if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
-                commitCurrentAutoCorrection(primaryCode, ic);
+            if (mCurrentSettings.mCorrectionEnabled && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
+                commitCurrentAutoCorrection(primaryCode);
                 didAutoCorrect = true;
             } else {
-                commitTyped(ic, primaryCode);
+                commitTyped(primaryCode);
             }
         }
 
-        final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
+        final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
                 KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
+                mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
             sendKeyCodePoint(Keyboard.CODE_SPACE);
         }
         sendKeyCodePoint(primaryCode);
 
         if (Keyboard.CODE_SPACE == primaryCode) {
-            if (isSuggestionsRequested()) {
-                if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
+            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+                if (maybeDoubleSpace()) {
                     mSpaceState = SPACE_STATE_DOUBLE;
                 } else if (!isShowingPunctuationList()) {
                     mSpaceState = SPACE_STATE_WEAK;
@@ -1657,13 +1583,13 @@
             }
 
             mHandler.startDoubleSpacesTimer();
-            if (!isCursorTouchingWord()) {
+            if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
                 mHandler.cancelUpdateSuggestions();
                 mHandler.postUpdateBigramPredictions();
             }
         } else {
             if (swapWeakSpace) {
-                swapSwapperAndSpaceWhileInBatchEdit(ic);
+                swapSwapperAndSpace();
                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
             } else if (SPACE_STATE_PHANTOM == spaceState) {
                 // If we are in phantom space state, and the user presses a separator, we want to
@@ -1682,9 +1608,6 @@
 
         Utils.Stats.onSeparator((char)primaryCode, x, y);
 
-        if (ic != null) {
-            ic.endBatchEdit();
-        }
         return didAutoCorrect;
     }
 
@@ -1695,27 +1618,16 @@
     }
 
     private void handleClose() {
-        commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
+        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
         requestHideSelf(0);
         LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
         if (inputView != null)
             inputView.closing();
     }
 
-    public boolean isSuggestionsRequested() {
-        return mInputAttributes.mIsSettingsSuggestionStripOn
-                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
-    }
-
     public boolean isShowingPunctuationList() {
         if (mSuggestionsView == null) return false;
-        return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
-    }
-
-    public boolean isShowingSuggestionsStrip() {
-        return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
-                || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
-                        && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
+        return mCurrentSettings.mSuggestPuncList == mSuggestionsView.getSuggestions();
     }
 
     public boolean isSuggestionsStripVisible() {
@@ -1723,11 +1635,11 @@
             return false;
         if (mSuggestionsView.isShowingAddToDictionaryHint())
             return true;
-        if (!isShowingSuggestionsStrip())
+        if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
             return false;
-        if (mInputAttributes.mApplicationSpecifiedCompletionOn)
+        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
             return true;
-        return isSuggestionsRequested();
+        return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation);
     }
 
     public void switchToKeyboardView() {
@@ -1765,20 +1677,18 @@
 
     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
         // Put a blue underline to a word in TextView which will be auto-corrected.
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return;
         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
                 && mWordComposer.isComposingWord()) {
             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
             final CharSequence textWithUnderline =
                     getTextWithUnderline(mWordComposer.getTypedWord());
-            ic.setComposingText(textWithUnderline, 1);
+            mConnection.setComposingText(textWithUnderline, 1);
         }
     }
 
     public void updateSuggestions() {
         // Check if we have a suggestion engine attached.
-        if ((mSuggest == null || !isSuggestionsRequested())) {
+        if ((mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation))) {
             if (mWordComposer.isComposingWord()) {
                 Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
                 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
@@ -1795,18 +1705,12 @@
         }
 
         // TODO: May need a better way of retrieving previous word
-        final InputConnection ic = getCurrentInputConnection();
-        final CharSequence prevWord;
-        if (null == ic) {
-            prevWord = null;
-        } else {
-            prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
-        }
-
+        final CharSequence prevWord = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
         final CharSequence typedWord = mWordComposer.getTypedWord();
         // getSuggestedWords handles gracefully a null value of prevWord
         final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
-                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
+                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
+                mCurrentSettings.mCorrectionEnabled);
 
         // Basically, we update the suggestion strip only when suggestion count > 1.  However,
         // there is an exception: We update the suggestion strip whenever typed word's length
@@ -1820,7 +1724,7 @@
             showSuggestions(suggestedWords, typedWord);
         } else {
             SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
-            if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
+            if (previousSuggestions == mCurrentSettings.mSuggestPuncList) {
                 previousSuggestions = SuggestedWords.EMPTY;
             }
             final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
@@ -1856,8 +1760,7 @@
         setSuggestionStripShown(isSuggestionsStripVisible());
     }
 
-    private void commitCurrentAutoCorrection(final int separatorCodePoint,
-            final InputConnection ic) {
+    private void commitCurrentAutoCorrection(final int separatorCodePoint) {
         // Complete any pending suggestions query first
         if (mHandler.hasPendingUpdateSuggestions()) {
             mHandler.cancelUpdateSuggestions();
@@ -1878,10 +1781,11 @@
             mExpectingUpdateSelection = true;
             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
                     separatorCodePoint);
-            if (!typedWord.equals(autoCorrection) && null != ic) {
+            if (!typedWord.equals(autoCorrection)) {
                 // This will make the correction flash for a short while as a visual clue
                 // to the user that auto-correction happened.
-                ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+                mConnection.commitCorrection(
+                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
                         typedWord, autoCorrection));
             }
         }
@@ -1889,15 +1793,7 @@
 
     @Override
     public void pickSuggestionManually(final int index, final CharSequence suggestion,
-            int x, int y) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (null != ic) ic.beginBatchEdit();
-        pickSuggestionManuallyWhileInBatchEdit(index, suggestion, x, y, ic);
-        if (null != ic) ic.endBatchEdit();
-    }
-
-    public void pickSuggestionManuallyWhileInBatchEdit(final int index,
-        final CharSequence suggestion, final int x, final int y, final InputConnection ic) {
+            final int x, final int y) {
         final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
         if (suggestion.length() == 1 && isShowingPunctuationList()) {
@@ -1917,13 +1813,13 @@
 
         if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
             int firstChar = Character.codePointAt(suggestion, 0);
-            if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
-                    && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
+            if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
+                    && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
                 sendKeyCodePoint(Keyboard.CODE_SPACE);
             }
         }
 
-        if (mInputAttributes.mApplicationSpecifiedCompletionOn
+        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
                 && mApplicationSpecifiedCompletions != null
                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
             if (mSuggestionsView != null) {
@@ -1931,13 +1827,13 @@
             }
             mKeyboardSwitcher.updateShiftState();
             resetComposingState(true /* alsoResetLastComposedWord */);
-            if (ic != null) {
-                final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
-                ic.commitCompletion(completionInfo);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
-                            completionInfo.getText(), x, y);
-                }
+            final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
+            mConnection.beginBatchEdit(getCurrentInputConnection());
+            mConnection.commitCompletion(completionInfo);
+            mConnection.endBatchEdit();
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
+                        completionInfo.getText(), x, y);
             }
             return;
         }
@@ -1966,8 +1862,6 @@
         // - There is a dictionary and the word is not in it
         // Please note that if mSuggest is null, it means that everything is off: suggestion
         // and correction, so we shouldn't try to show the hint
-        // We used to look at mCorrectionMode here, but showing the hint should have nothing
-        // to do with the autocorrection setting.
         final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
                 // If there is no dictionary the hint should be shown.
                 && (!mSuggest.hasMainDictionary()
@@ -1987,7 +1881,7 @@
         } else {
             if (mIsUserDictionaryAvailable) {
                 mSuggestionsView.showAddToDictionaryHint(
-                        suggestion, mSettingsValues.mHintToSaveText);
+                        suggestion, mCurrentSettings.mHintToSaveText);
             } else {
                 mHandler.postUpdateSuggestions();
             }
@@ -1999,22 +1893,11 @@
      */
     private void commitChosenWord(final CharSequence chosenWord, final int commitType,
             final int separatorCode) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            if (mSettingsValues.mEnableSuggestionSpanInsertion) {
-                final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
-                ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
-                        this, chosenWord, suggestedWords, mIsMainDictionaryAvailable),
-                        1);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_commitText(chosenWord);
-                }
-            } else {
-                ic.commitText(chosenWord, 1);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_commitText(chosenWord);
-                }
-            }
+        final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
+        mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
+                this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_commitText(chosenWord);
         }
         // Add the word to the user history dictionary
         final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
@@ -2027,18 +1910,17 @@
     }
 
     public void updateBigramPredictions() {
-        if (mSuggest == null || !isSuggestionsRequested())
+        if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation))
             return;
 
-        if (!mSettingsValues.mBigramPredictionEnabled) {
+        if (!mCurrentSettings.mBigramPredictionEnabled) {
             setPunctuationSuggestions();
             return;
         }
 
         final SuggestedWords suggestedWords;
-        if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
-            final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
-                    mSettingsValues.mWordSeparators);
+        if (mCurrentSettings.mCorrectionEnabled) {
+            final CharSequence prevWord = mConnection.getThisWord(mCurrentSettings.mWordSeparators);
             if (!TextUtils.isEmpty(prevWord)) {
                 suggestedWords = mSuggest.getBigramPredictions(prevWord);
             } else {
@@ -2058,10 +1940,10 @@
     }
 
     public void setPunctuationSuggestions() {
-        if (mSettingsValues.mBigramPredictionEnabled) {
+        if (mCurrentSettings.mBigramPredictionEnabled) {
             clearSuggestions();
         } else {
-            setSuggestions(mSettingsValues.mSuggestPuncList, false);
+            setSuggestions(mCurrentSettings.mSuggestPuncList, false);
         }
         setAutoCorrectionIndicator(false);
         setSuggestionStripShown(isSuggestionsStripVisible());
@@ -2070,22 +1952,15 @@
     private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
         if (TextUtils.isEmpty(suggestion)) return null;
 
-        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
-        // adding words in situations where the user or application really didn't
-        // want corrections enabled or learned.
-        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
-                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
-            return null;
-        }
+        // If correction is not enabled, we don't add words to the user history dictionary.
+        // That's to avoid unintended additions in some sensitive fields, or fields that
+        // expect to receive non-words.
+        if (!mCurrentSettings.mCorrectionEnabled) return null;
 
-        if (mUserHistoryDictionary != null) {
-            final InputConnection ic = getCurrentInputConnection();
-            final CharSequence prevWord;
-            if (null != ic) {
-                prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
-            } else {
-                prevWord = null;
-            }
+        final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
+        if (userHistoryDictionary != null) {
+            final CharSequence prevWord
+                    = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
             final String secondWord;
             if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
                 secondWord = suggestion.toString().toLowerCase(
@@ -2098,95 +1973,36 @@
             final int maxFreq = AutoCorrection.getMaxFrequency(
                     mSuggest.getUnigramDictionaries(), suggestion);
             if (maxFreq == 0) return null;
-            mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
+            userHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
                     secondWord, maxFreq > 0);
             return prevWord;
         }
         return null;
     }
 
-    public boolean isCursorTouchingWord() {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return false;
-        CharSequence before = ic.getTextBeforeCursor(1, 0);
-        CharSequence after = ic.getTextAfterCursor(1, 0);
-        if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))
-                && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
-            return true;
-        }
-        if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))
-                && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
-            return true;
-        }
-        return false;
-    }
-
-    // "ic" must not be null
-    private static boolean sameAsTextBeforeCursor(final InputConnection ic,
-            final CharSequence text) {
-        final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
-        return TextUtils.equals(text, beforeText);
-    }
-
-    // "ic" must not be null
     /**
      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
      * word, else do nothing.
      */
-    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
-            final InputConnection ic) {
-        // Bail out if the cursor is not at the end of a word (cursor must be preceded by
-        // non-whitespace, non-separator, non-start-of-text)
-        // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
-        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
-        if (TextUtils.isEmpty(textBeforeCursor)
-                || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
-
-        // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
-        // separator or end of line/text)
-        // Example: "test|"<EOL> "te|st" get rejected here
-        final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
-        if (!TextUtils.isEmpty(textAfterCursor)
-                && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
-
-        // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
-        // Example: " -|" gets rejected here but "e-|" and "e|" are okay
-        CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
-        // We don't suggest on leading single quotes, so we have to remove them from the word if
-        // it starts with single quotes.
-        while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
-            word = word.subSequence(1, word.length());
+    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
+        final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings);
+        if (null != word) {
+            restartSuggestionsOnWordBeforeCursor(word);
         }
-        if (TextUtils.isEmpty(word)) return;
-        final char firstChar = word.charAt(0); // we just tested that word is not empty
-        if (word.length() == 1 && !Character.isLetter(firstChar)) return;
-
-        // We only suggest on words that start with a letter or a symbol that is excluded from
-        // word separators (see #handleCharacterWhileInBatchEdit).
-        if (!(isAlphabet(firstChar)
-                || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
-            return;
-        }
-
-        // Okay, we are at the end of a word. Restart suggestions.
-        restartSuggestionsOnWordBeforeCursor(ic, word);
     }
 
-    // "ic" must not be null
-    private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
-            final CharSequence word) {
+    private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
         final int length = word.length();
-        ic.deleteSurroundingText(length, 0);
+        mConnection.deleteSurroundingText(length, 0);
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_deleteSurroundingText(length);
         }
-        ic.setComposingText(word, 1);
+        mConnection.setComposingText(word, 1);
         mHandler.postUpdateSuggestions();
     }
 
-    // "ic" must not be null
-    private void revertCommit(final InputConnection ic) {
+    private void revertCommit() {
         final CharSequence previousWord = mLastComposedWord.mPrevWord;
         final String originallyTypedWord = mLastComposedWord.mTypedWord;
         final CharSequence committedWord = mLastComposedWord.mCommittedWord;
@@ -2200,7 +2016,7 @@
                 throw new RuntimeException("revertCommit, but we are composing a word");
             }
             final String wordBeforeCursor =
-                    ic.getTextBeforeCursor(deleteLength, 0)
+                    mConnection.getTextBeforeCursor(deleteLength, 0)
                             .subSequence(0, cancelLength).toString();
             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
                 throw new RuntimeException("revertCommit check failed: we thought we were "
@@ -2208,7 +2024,7 @@
                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
             }
         }
-        ic.deleteSurroundingText(deleteLength, 0);
+        mConnection.deleteSurroundingText(deleteLength, 0);
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
         }
@@ -2220,9 +2036,9 @@
             // This is the case when we cancel a manual pick.
             // We should restart suggestion on the word right away.
             mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
-            ic.setComposingText(originallyTypedWord, 1);
+            mConnection.setComposingText(originallyTypedWord, 1);
         } else {
-            ic.commitText(originallyTypedWord, 1);
+            mConnection.commitText(originallyTypedWord, 1);
             // Re-insert the separator
             sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
             Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
@@ -2238,61 +2054,8 @@
         mHandler.postUpdateSuggestions();
     }
 
-    // "ic" must not be null
-    private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
-        mHandler.cancelDoubleSpacesTimer();
-        // Here we test whether we indeed have a period and a space before us. This should not
-        // be needed, but it's there just in case something went wrong.
-        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
-        if (!". ".equals(textBeforeCursor)) {
-            // Theoretically we should not be coming here if there isn't ". " before the
-            // cursor, but the application may be changing the text while we are typing, so
-            // anything goes. We should not crash.
-            Log.d(TAG, "Tried to revert double-space combo but we didn't find "
-                    + "\". \" just before the cursor.");
-            return false;
-        }
-        ic.deleteSurroundingText(2, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(2);
-        }
-        ic.commitText("  ", 1);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
-        }
-        return true;
-    }
-
-    private static boolean revertSwapPunctuation(final InputConnection ic) {
-        // Here we test whether we indeed have a space and something else before us. This should not
-        // be needed, but it's there just in case something went wrong.
-        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
-        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
-        // enter surrogate pairs this code will have been removed.
-        if (TextUtils.isEmpty(textBeforeCursor)
-                || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
-            // We may only come here if the application is changing the text while we are typing.
-            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
-            // but some debugging log may be in order.
-            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
-                    + "find a space just before the cursor.");
-            return false;
-        }
-        ic.beginBatchEdit();
-        ic.deleteSurroundingText(2, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(2);
-        }
-        ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_revertSwapPunctuation();
-        }
-        ic.endBatchEdit();
-        return true;
-    }
-
     public boolean isWordSeparator(int code) {
-        return mSettingsValues.isWordSeparator(code);
+        return mCurrentSettings.isWordSeparator(code);
     }
 
     public boolean preferCapitalization() {
@@ -2306,15 +2069,14 @@
         // onConfigurationChanged before SoftInputWindow is shown.
         if (mKeyboardSwitcher.getKeyboardView() != null) {
             // Reload keyboard because the current language has been changed.
-            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
+            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
         }
         initSuggest();
-        updateCorrectionMode();
         loadSettings();
         // Since we just changed languages, we should re-evaluate suggestions with whatever word
         // we are currently composing. If we are not composing anything, we may want to display
         // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
-        if (isCursorTouchingWord()) {
+        if (mConnection.isCursorTouchingWord(mCurrentSettings)) {
             mHandler.postUpdateSuggestions();
         } else {
             mHandler.postUpdateBigramPredictions();
@@ -2352,12 +2114,9 @@
             // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
             // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
             // pair-friendly way of deleting characters in InputConnection.
-            final InputConnection ic = getCurrentInputConnection();
-            if (null != ic) {
-                final CharSequence lastChar = ic.getTextBeforeCursor(1, 0);
-                if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
-                    ic.deleteSurroundingText(1, 0);
-                }
+            final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
+            if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
+                mConnection.deleteSurroundingText(1, 0);
             }
         }
     }
@@ -2375,25 +2134,6 @@
         }
     };
 
-    private void updateCorrectionMode() {
-        // TODO: cleanup messy flags
-        final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
-                && !mInputAttributes.mInputTypeNoAutoCorrect;
-        mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
-        mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
-                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
-    }
-
-    private void updateSuggestionVisibility(final Resources res) {
-        final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
-        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
-            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
-                mSuggestionVisibility = visibility;
-                break;
-            }
-        }
-    }
-
     private void launchSettings() {
         launchSettingsClass(SettingsActivity.class);
     }
@@ -2440,10 +2180,10 @@
         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
                 .setItems(items, listener)
                 .setTitle(title);
-        showOptionDialogInternal(builder.create());
+        showOptionDialog(builder.create());
     }
 
-    private void showOptionDialogInternal(AlertDialog dialog) {
+    /* package */ void showOptionDialog(AlertDialog dialog) {
         final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
         if (windowToken == null) return;
 
@@ -2470,13 +2210,13 @@
         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
         p.println("  Keyboard mode = " + keyboardMode);
-        p.println("  mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
-        p.println("  mCorrectionMode=" + mCorrectionMode);
+        p.println("  mIsSuggestionsSuggestionsRequested = "
+                + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation));
+        p.println("  mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled);
         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
-        p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
-        p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
-        p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
-        p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
-        p.println("  mInputAttributes=" + mInputAttributes.toString());
+        p.println("  mSoundOn=" + mCurrentSettings.mSoundOn);
+        p.println("  mVibrateOn=" + mCurrentSettings.mVibrateOn);
+        p.println("  mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
+        p.println("  inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/NativeUtils.java b/java/src/com/android/inputmethod/latin/NativeUtils.java
new file mode 100644
index 0000000..9cc2bc0
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/NativeUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class NativeUtils {
+    static {
+        JniUtils.loadNativeLibrary();
+    }
+
+    private NativeUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    /**
+     * This method just calls up libm's powf() directly.
+     */
+    public static native float powf(float x, float y);
+}
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
index 66d6d58..cf3cc78 100644
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java
@@ -16,37 +16,47 @@
 
 package com.android.inputmethod.latin;
 
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
 import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
 import android.os.SystemClock;
-import android.preference.PreferenceManager;
 import android.text.TextUtils;
+import android.util.JsonWriter;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.Toast;
 
 import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.internal.KeyboardState;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.RichInputConnection.Range;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.charset.Charset;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.UUID;
 
 /**
  * Logs the use of the LatinIME keyboard.
@@ -58,198 +68,84 @@
  */
 public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
     private static final String TAG = ResearchLogger.class.getSimpleName();
-    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
     private static final boolean DEBUG = false;
+    private static final boolean OUTPUT_ENTIRE_BUFFER = false;  // true may disclose private info
+    /* package */ static boolean sIsLogging = false;
+    private static final int OUTPUT_FORMAT_VERSION = 1;
+    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+    private static final String FILENAME_PREFIX = "researchLog";
+    private static final String FILENAME_SUFFIX = ".txt";
+    private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
+            new OutputStreamWriter(new NullOutputStream()));
+    private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
+            new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);
 
-    private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager());
-    public static boolean sIsLogging = false;
-    /* package */ final Handler mLoggingHandler;
-    private InputMethodService mIms;
+    // constants related to specific log points
+    private static final String WHITESPACE_SEPARATORS = " \t\n\r";
+    private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
+    private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
 
-    /**
-     * Isolates management of files. This variable should never be null, but can be changed
-     * to support testing.
-     */
-    /* package */ LogFileManager mLogFileManager;
+    private static final ResearchLogger sInstance = new ResearchLogger();
+    private HandlerThread mHandlerThread;
+    /* package */ Handler mLoggingHandler;
+    // to write to a different filename, e.g., for testing, set mFile before calling start()
+    private File mFilesDir;
+    /* package */ File mFile;
+    private JsonWriter mJsonWriter = NULL_JSON_WRITER; // should never be null
 
-    /**
-     * Manages the file(s) that stores the logs.
-     *
-     * Handles creation, deletion, and provides Readers, Writers, and InputStreams to access
-     * the logs.
-     */
-    /* package */ static class LogFileManager {
-        public static final String RESEARCH_LOG_FILENAME_KEY = "RESEARCH_LOG_FILENAME";
+    private int mLoggingState;
+    private static final int LOGGING_STATE_OFF = 0;
+    private static final int LOGGING_STATE_ON = 1;
+    private static final int LOGGING_STATE_STOPPING = 2;
+    private boolean mIsPasswordView = false;
 
-        private static final String DEFAULT_FILENAME = "researchLog.txt";
-        private static final long LOGFILE_PURGE_INTERVAL = 1000 * 60 * 60 * 24;
+    // digits entered by the user are replaced with this codepoint.
+    /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
+            Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
+    // U+E001 is in the "private-use area"
+    /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
+    // set when LatinIME should ignore an onUpdateSelection() callback that
+    // arises from operations in this class
+    private static boolean sLatinIMEExpectingUpdateSelection = false;
 
-        protected InputMethodService mIms;
-        protected File mFile;
-        protected PrintWriter mPrintWriter;
+    // used to check whether words are not unique
+    private Suggest mSuggest;
+    private Dictionary mDictionary;
 
-        /* package */ LogFileManager() {
+    private static class NullOutputStream extends OutputStream {
+        /** {@inheritDoc} */
+        @Override
+        public void write(byte[] buffer, int offset, int count) {
+            // nop
         }
 
-        public void init(final InputMethodService ims) {
-            mIms = ims;
+        /** {@inheritDoc} */
+        @Override
+        public void write(byte[] buffer) {
+            // nop
         }
 
-        public synchronized void createLogFile() throws IOException {
-            createLogFile(DEFAULT_FILENAME);
-        }
-
-        public synchronized void createLogFile(final SharedPreferences prefs)
-                throws IOException {
-            final String filename =
-                    prefs.getString(RESEARCH_LOG_FILENAME_KEY, DEFAULT_FILENAME);
-            createLogFile(filename);
-        }
-
-        public synchronized void createLogFile(final String filename)
-                throws IOException {
-            if (mIms == null) {
-                final String msg = "InputMethodService is not configured.  Logging is off.";
-                Log.w(TAG, msg);
-                throw new IOException(msg);
-            }
-            final File filesDir = mIms.getFilesDir();
-            if (filesDir == null || !filesDir.exists()) {
-                final String msg = "Storage directory does not exist.  Logging is off.";
-                Log.w(TAG, msg);
-                throw new IOException(msg);
-            }
-            close();
-            final File file = new File(filesDir, filename);
-            mFile = file;
-            boolean append = true;
-            if (file.exists() && file.lastModified() + LOGFILE_PURGE_INTERVAL <
-                    System.currentTimeMillis()) {
-                append = false;
-            }
-            mPrintWriter = new PrintWriter(new BufferedWriter(new FileWriter(file, append)), true);
-        }
-
-        public synchronized boolean append(final String s) {
-            PrintWriter printWriter = mPrintWriter;
-            if (printWriter == null || !mFile.exists()) {
-                if (DEBUG) {
-                    Log.w(TAG, "PrintWriter is null... attempting to create default log file");
-                }
-                try {
-                    createLogFile();
-                    printWriter = mPrintWriter;
-                } catch (IOException e) {
-                    Log.w(TAG, "Failed to create log file.  Not logging.");
-                    return false;
-                }
-            }
-            printWriter.print(s);
-            printWriter.flush();
-            return !printWriter.checkError();
-        }
-
-        public synchronized void reset() {
-            if (mPrintWriter != null) {
-                mPrintWriter.close();
-                mPrintWriter = null;
-                if (DEBUG) {
-                    Log.d(TAG, "logfile closed");
-                }
-            }
-            if (mFile != null) {
-                mFile.delete();
-                if (DEBUG) {
-                    Log.d(TAG, "logfile deleted");
-                }
-                mFile = null;
-            }
-        }
-
-        public synchronized void close() {
-            if (mPrintWriter != null) {
-                mPrintWriter.close();
-                mPrintWriter = null;
-                mFile = null;
-                if (DEBUG) {
-                    Log.d(TAG, "logfile closed");
-                }
-            }
-        }
-
-        /* package */ synchronized void flush() {
-            if (mPrintWriter != null) {
-                mPrintWriter.flush();
-            }
-        }
-
-        /* package */ synchronized String getContents() {
-            final File file = mFile;
-            if (file == null) {
-                return "";
-            }
-            if (mPrintWriter != null) {
-                mPrintWriter.flush();
-            }
-            FileInputStream stream = null;
-            FileChannel fileChannel = null;
-            String s = "";
-            try {
-                stream = new FileInputStream(file);
-                fileChannel = stream.getChannel();
-                final ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
-                fileChannel.read(byteBuffer);
-                byteBuffer.rewind();
-                CharBuffer charBuffer = Charset.defaultCharset().decode(byteBuffer);
-                s = charBuffer.toString();
-            } catch (IOException e) {
-                e.printStackTrace();
-            } finally {
-                try {
-                    if (fileChannel != null) {
-                        fileChannel.close();
-                    }
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } finally {
-                    try {
-                        if (stream != null) {
-                            stream.close();
-                        }
-                    } catch (IOException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-            return s;
+        @Override
+        public void write(int oneByte) {
         }
     }
 
-    private ResearchLogger(final LogFileManager logFileManager) {
-        final HandlerThread handlerThread = new HandlerThread("ResearchLogger logging task",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        handlerThread.start();
-        mLoggingHandler = new Handler(handlerThread.getLooper());
-        mLogFileManager = logFileManager;
+    private ResearchLogger() {
+        mLoggingState = LOGGING_STATE_OFF;
     }
 
     public static ResearchLogger getInstance() {
         return sInstance;
     }
 
-    public static void init(final InputMethodService ims, final SharedPreferences prefs) {
-        sInstance.initInternal(ims, prefs);
-    }
-
-    /* package */ void initInternal(final InputMethodService ims, final SharedPreferences prefs) {
-        mIms = ims;
-        final LogFileManager logFileManager = mLogFileManager;
-        if (logFileManager != null) {
-            logFileManager.init(ims);
-            try {
-                logFileManager.createLogFile(prefs);
-            } catch (IOException e) {
-                e.printStackTrace();
+    public void init(final InputMethodService ims, final SharedPreferences prefs) {
+        assert ims != null;
+        if (ims == null) {
+            Log.w(TAG, "IMS is null; logging is off");
+        } else {
+            mFilesDir = ims.getFilesDir();
+            if (mFilesDir == null || !mFilesDir.exists()) {
+                Log.w(TAG, "IME storage directory does not exist.");
             }
         }
         if (prefs != null) {
@@ -258,173 +154,124 @@
         }
     }
 
-    /**
-     * Represents a category of logging events that share the same subfield structure.
-     */
-    private static enum LogGroup {
-        MOTION_EVENT("m"),
-        KEY("k"),
-        CORRECTION("c"),
-        STATE_CHANGE("s"),
-        UNSTRUCTURED("u");
-
-        private final String mLogString;
-
-        private LogGroup(final String logString) {
-            mLogString = logString;
+    public synchronized void start() {
+        Log.d(TAG, "start called");
+        if (!sIsLogging) {
+            // Log.w(TAG, "not in usability mode; not logging");
+            return;
         }
-    }
-
-    public void logMotionEvent(final int action, final long eventTime, final int id,
-            final int x, final int y, final float size, final float pressure) {
-        final String eventTag;
-        switch (action) {
-            case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break;
-            case MotionEvent.ACTION_UP: eventTag = "[Up]"; break;
-            case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break;
-            case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break;
-            case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break;
-            case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break;
-            case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break;
-            default: eventTag = "[Action" + action + "]"; break;
-        }
-        if (!TextUtils.isEmpty(eventTag)) {
-            final StringBuilder sb = new StringBuilder();
-            sb.append(eventTag);
-            sb.append('\t'); sb.append(eventTime);
-            sb.append('\t'); sb.append(id);
-            sb.append('\t'); sb.append(x);
-            sb.append('\t'); sb.append(y);
-            sb.append('\t'); sb.append(size);
-            sb.append('\t'); sb.append(pressure);
-            write(LogGroup.MOTION_EVENT, sb.toString());
-        }
-    }
-
-    public void logKeyEvent(final int code, final int x, final int y) {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(Keyboard.printableCode(code));
-        sb.append('\t'); sb.append(x);
-        sb.append('\t'); sb.append(y);
-        write(LogGroup.KEY, sb.toString());
-    }
-
-    public void logCorrection(final String subgroup, final String before, final String after,
-            final int position) {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(subgroup);
-        sb.append('\t'); sb.append(before);
-        sb.append('\t'); sb.append(after);
-        sb.append('\t'); sb.append(position);
-        write(LogGroup.CORRECTION, sb.toString());
-    }
-
-    public void logStateChange(final String subgroup, final String details) {
-        write(LogGroup.STATE_CHANGE, subgroup + "\t" + details);
-    }
-
-    public static class UnsLogGroup {
-        private static final boolean DEFAULT_ENABLED = true;
-
-        private static final boolean KEYBOARDSTATE_ONCANCELINPUT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONCODEINPUT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONPRESSKEY_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONRELEASEKEY_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_COMMITTEXT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_DELETESURROUNDINGTEXT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_ONDISPLAYCOMPLETIONS_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_ONUPDATESELECTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PERFORMEDITORACTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PICKSUGGESTIONMANUALLY_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_REVERTCOMMIT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINIME_REVERTSWAPPUNCTUATION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_SENDKEYCODEPOINT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean
-                POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_ONDOWNEVENT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_ONMOVEEVENT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED = DEFAULT_ENABLED;
-    }
-
-    public static void logUnstructured(String logGroup, final String details) {
-        // TODO: improve performance by making entire class static and/or implementing natively
-        getInstance().write(LogGroup.UNSTRUCTURED, logGroup + "\t" + details);
-    }
-
-    private void write(final LogGroup logGroup, final String log) {
-        // TODO: rewrite in native for better performance
-        mLoggingHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                final long currentTime = System.currentTimeMillis();
-                final long upTime = SystemClock.uptimeMillis();
-                final StringBuilder builder = new StringBuilder();
-                builder.append(currentTime);
-                builder.append('\t'); builder.append(upTime);
-                builder.append('\t'); builder.append(logGroup.mLogString);
-                builder.append('\t'); builder.append(log);
-                builder.append('\n');
-                if (DEBUG) {
-                    Log.d(TAG, "Write: " + '[' + logGroup.mLogString + ']' + log);
+        if (mFilesDir == null || !mFilesDir.exists()) {
+            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
+        } else {
+            if (mHandlerThread == null || !mHandlerThread.isAlive()) {
+                mHandlerThread = new HandlerThread("ResearchLogger logging task",
+                        Process.THREAD_PRIORITY_BACKGROUND);
+                mHandlerThread.start();
+                mLoggingHandler = null;
+                mLoggingState = LOGGING_STATE_OFF;
+            }
+            if (mLoggingHandler == null) {
+                mLoggingHandler = new Handler(mHandlerThread.getLooper());
+                mLoggingState = LOGGING_STATE_OFF;
+            }
+            if (mFile == null) {
+                final String timestampString = TIMESTAMP_DATEFORMAT.format(new Date());
+                mFile = new File(mFilesDir, FILENAME_PREFIX + timestampString + FILENAME_SUFFIX);
+            }
+            if (mLoggingState == LOGGING_STATE_OFF) {
+                try {
+                    mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
+                    mJsonWriter.setLenient(true);
+                    mJsonWriter.beginArray();
+                    mLoggingState = LOGGING_STATE_ON;
+                } catch (IOException e) {
+                    Log.w(TAG, "cannot start JsonWriter");
+                    mJsonWriter = NULL_JSON_WRITER;
+                    e.printStackTrace();
                 }
-                final String s = builder.toString();
-                if (mLogFileManager.append(s)) {
-                    // success
-                } else {
-                    if (DEBUG) {
-                        Log.w(TAG, "Unable to write to log.");
-                    }
-                    // perhaps logfile was deleted.  try to recreate and relog.
+            }
+        }
+    }
+
+    public synchronized void stop() {
+        Log.d(TAG, "stop called");
+        if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) {
+            mLoggingState = LOGGING_STATE_STOPPING;
+            // put this in the Handler queue so pending writes are processed first.
+            mLoggingHandler.post(new Runnable() {
+                @Override
+                public void run() {
                     try {
-                        mLogFileManager.createLogFile(PreferenceManager
-                                .getDefaultSharedPreferences(mIms));
-                        mLogFileManager.append(s);
+                        Log.d(TAG, "closing jsonwriter");
+                        mJsonWriter.endArray();
+                        mJsonWriter.flush();
+                        mJsonWriter.close();
+                    } catch (IllegalStateException e1) {
+                        // assume that this is just the json not being terminated properly.
+                        // ignore
+                        e1.printStackTrace();
                     } catch (IOException e) {
                         e.printStackTrace();
+                    } finally {
+                        mJsonWriter = NULL_JSON_WRITER;
+                        mFile = null;
+                        mLoggingState = LOGGING_STATE_OFF;
+                        if (DEBUG) {
+                            Log.d(TAG, "logfile closed");
+                        }
+                        Log.d(TAG, "finished stop(), notifying");
+                        synchronized (ResearchLogger.this) {
+                            ResearchLogger.this.notify();
+                        }
                     }
                 }
+            });
+            try {
+                wait();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
             }
-        });
+        }
     }
 
-    public void clearAll() {
-        mLoggingHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (DEBUG) {
-                    Log.d(TAG, "Delete log file.");
+    public synchronized boolean abort() {
+        Log.d(TAG, "abort called");
+        boolean isLogFileDeleted = false;
+        if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) {
+            mLoggingState = LOGGING_STATE_STOPPING;
+            try {
+                Log.d(TAG, "closing jsonwriter");
+                mJsonWriter.endArray();
+                mJsonWriter.close();
+            } catch (IllegalStateException e1) {
+                // assume that this is just the json not being terminated properly.
+                // ignore
+                e1.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            } finally {
+                mJsonWriter = NULL_JSON_WRITER;
+                // delete file
+                final boolean isDeleted = mFile.delete();
+                if (isDeleted) {
+                    isLogFileDeleted = true;
                 }
-                mLogFileManager.reset();
+                mFile = null;
+                mLoggingState = LOGGING_STATE_OFF;
+                if (DEBUG) {
+                    Log.d(TAG, "logfile closed");
+                }
             }
-        });
+        }
+        return isLogFileDeleted;
     }
 
-    /* package */ LogFileManager getLogFileManager() {
-        return mLogFileManager;
+    /* package */ synchronized void flush() {
+        try {
+            mJsonWriter.flush();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
     }
 
     @Override
@@ -433,325 +280,764 @@
             return;
         }
         sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
-    }
-
-    public static void keyboardState_onCancelInput(final boolean isSinglePointer,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONCANCELINPUT_ENABLED) {
-            final String s = "onCancelInput: single=" + isSinglePointer + " " + keyboardState;
-            logUnstructured("KeyboardState_onCancelInput", s);
+        if (sIsLogging == false) {
+            abort();
         }
     }
 
-    public static void keyboardState_onCodeInput(
-            final int code, final boolean isSinglePointer, final int autoCaps,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONCODEINPUT_ENABLED) {
-            final String s = "onCodeInput: code=" + Keyboard.printableCode(code)
-                    + " single=" + isSinglePointer
-                    + " autoCaps=" + autoCaps + " " + keyboardState;
-            logUnstructured("KeyboardState_onCodeInput", s);
-        }
-    }
-
-    public static void keyboardState_onLongPressTimeout(final int code,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED) {
-            final String s = "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " "
-                    + keyboardState;
-            logUnstructured("KeyboardState_onLongPressTimeout", s);
-        }
-    }
-
-    public static void keyboardState_onPressKey(final int code,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONPRESSKEY_ENABLED) {
-            final String s = "onPressKey: code=" + Keyboard.printableCode(code) + " "
-                    + keyboardState;
-            logUnstructured("KeyboardState_onPressKey", s);
-        }
-    }
-
-    public static void keyboardState_onReleaseKey(final KeyboardState keyboardState, final int code,
-            final boolean withSliding) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONRELEASEKEY_ENABLED) {
-            final String s = "onReleaseKey: code=" + Keyboard.printableCode(code)
-                    + " sliding=" + withSliding + " " + keyboardState;
-            logUnstructured("KeyboardState_onReleaseKey", s);
-        }
-    }
-
-    public static void latinIME_commitCurrentAutoCorrection(final String typedWord,
-            final String autoCorrection) {
-        if (UnsLogGroup.LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED) {
-            if (typedWord.equals(autoCorrection)) {
-                getInstance().logCorrection("[----]", typedWord, autoCorrection, -1);
-            } else {
-                getInstance().logCorrection("[Auto]", typedWord, autoCorrection, -1);
-            }
-        }
-    }
-
-    public static void latinIME_commitText(final CharSequence typedWord) {
-        if (UnsLogGroup.LATINIME_COMMITTEXT_ENABLED) {
-            logUnstructured("LatinIME_commitText", typedWord.toString());
-        }
-    }
-
-    public static void latinIME_deleteSurroundingText(final int length) {
-        if (UnsLogGroup.LATINIME_DELETESURROUNDINGTEXT_ENABLED) {
-            logUnstructured("LatinIME_deleteSurroundingText", String.valueOf(length));
-        }
-    }
-
-    public static void latinIME_doubleSpaceAutoPeriod() {
-        if (UnsLogGroup.LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED) {
-            logUnstructured("LatinIME_doubleSpaceAutoPeriod", "");
-        }
-    }
-
-    public static void latinIME_onDisplayCompletions(
-            final CompletionInfo[] applicationSpecifiedCompletions) {
-        if (UnsLogGroup.LATINIME_ONDISPLAYCOMPLETIONS_ENABLED) {
-            final StringBuilder builder = new StringBuilder();
-            builder.append("Received completions:");
-            if (applicationSpecifiedCompletions != null) {
-                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
-                    builder.append("  #");
-                    builder.append(i);
-                    builder.append(": ");
-                    builder.append(applicationSpecifiedCompletions[i]);
-                    builder.append("\n");
+    /* package */ void presentResearchDialog(final LatinIME latinIME) {
+        final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
+        final CharSequence[] items = new CharSequence[] {
+                latinIME.getString(R.string.note_timestamp_for_researchlog),
+                latinIME.getString(R.string.do_not_log_this_session),
+        };
+        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface di, int position) {
+                di.dismiss();
+                switch (position) {
+                    case 0:
+                        ResearchLogger.getInstance().userTimestamp();
+                        Toast.makeText(latinIME, R.string.notify_recorded_timestamp,
+                                Toast.LENGTH_LONG).show();
+                        break;
+                    case 1:
+                        Toast toast = Toast.makeText(latinIME,
+                                R.string.notify_session_log_deleting, Toast.LENGTH_LONG);
+                        toast.show();
+                        final ResearchLogger logger = ResearchLogger.getInstance();
+                        boolean isLogDeleted = logger.abort();
+                        toast.cancel();
+                        if (isLogDeleted) {
+                            Toast.makeText(latinIME, R.string.notify_session_log_deleted,
+                                    Toast.LENGTH_LONG).show();
+                        } else {
+                            Toast.makeText(latinIME,
+                                    R.string.notify_session_log_not_deleted, Toast.LENGTH_LONG)
+                                    .show();
+                        }
+                        break;
                 }
             }
-            logUnstructured("LatinIME_onDisplayCompletions", builder.toString());
+        };
+        final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
+                .setItems(items, listener)
+                .setTitle(title);
+        latinIME.showOptionDialog(builder.create());
+    }
+
+    public void initSuggest(Suggest suggest) {
+        mSuggest = suggest;
+    }
+
+    private void setIsPasswordView(boolean isPasswordView) {
+        mIsPasswordView = isPasswordView;
+    }
+
+    private boolean isAllowedToLog() {
+        return mLoggingState == LOGGING_STATE_ON && !mIsPasswordView;
+    }
+
+    private static final String CURRENT_TIME_KEY = "_ct";
+    private static final String UPTIME_KEY = "_ut";
+    private static final String EVENT_TYPE_KEY = "_ty";
+    private static final Object[] EVENTKEYS_NULLVALUES = {};
+
+    private LogUnit mCurrentLogUnit = new LogUnit();
+
+    /**
+     * Buffer a research log event, flagging it as privacy-sensitive.
+     *
+     * This event contains potentially private information.  If the word that this event is a part
+     * of is determined to be privacy-sensitive, then this event should not be included in the
+     * output log.  The system waits to output until the containing word is known.
+     *
+     * @param keys an array containing a descriptive name for the event, followed by the keys
+     * @param values an array of values, either a String or Number.  length should be one
+     * less than the keys array
+     */
+    private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys,
+            final Object[] values) {
+        assert values.length + 1 == keys.length;
+        mCurrentLogUnit.addLogAtom(keys, values, true);
+    }
+
+    /**
+     * Buffer a research log event, flaggint it as not privacy-sensitive.
+     *
+     * This event contains no potentially private information.  Even if the word that this event
+     * is privacy-sensitive, this event can still safely be sent to the output log.  The system
+     * waits until the containing word is known so that this event can be written in the proper
+     * temporal order with other events that may be privacy sensitive.
+     *
+     * @param keys an array containing a descriptive name for the event, followed by the keys
+     * @param values an array of values, either a String or Number.  length should be one
+     * less than the keys array
+     */
+    private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
+        assert values.length + 1 == keys.length;
+        mCurrentLogUnit.addLogAtom(keys, values, false);
+    }
+
+    /* package for test */ boolean isPrivacyThreat(String word) {
+        // currently: word not in dictionary or contains numbers.
+        if (TextUtils.isEmpty(word)) {
+            return false;
+        }
+        final int length = word.length();
+        boolean hasLetter = false;
+        for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+            final int codePoint = Character.codePointAt(word, i);
+            if (Character.isDigit(codePoint)) {
+                return true;
+            }
+            if (Character.isLetter(codePoint)) {
+                hasLetter = true;
+                break; // Word may contain digits, but will only be allowed if in the dictionary.
+            }
+        }
+        if (hasLetter) {
+            if (mDictionary == null && mSuggest != null && mSuggest.hasMainDictionary()) {
+                mDictionary = mSuggest.getMainDictionary();
+            }
+            if (mDictionary == null) {
+                // Can't access dictionary.  Assume privacy threat.
+                return true;
+            }
+            return !(mDictionary.isValidWord(word));
+        }
+        // No letters, no numbers.  Punctuation, space, or something else.
+        return false;
+    }
+
+    /**
+     * Write out enqueued LogEvents to the log, possibly dropping privacy sensitive events.
+     */
+    /* package for test */ synchronized void flushQueue(boolean removePotentiallyPrivateEvents) {
+        if (isAllowedToLog()) {
+            mCurrentLogUnit.setRemovePotentiallyPrivateEvents(removePotentiallyPrivateEvents);
+            mLoggingHandler.post(mCurrentLogUnit);
+            mCurrentLogUnit = new LogUnit();
         }
     }
 
+    private synchronized void outputEvent(final String[] keys, final Object[] values) {
+        try {
+            mJsonWriter.beginObject();
+            mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
+            mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
+            mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
+            final int length = values.length;
+            for (int i = 0; i < length; i++) {
+                mJsonWriter.name(keys[i + 1]);
+                Object value = values[i];
+                if (value instanceof String) {
+                    mJsonWriter.value((String) value);
+                } else if (value instanceof Number) {
+                    mJsonWriter.value((Number) value);
+                } else if (value instanceof Boolean) {
+                    mJsonWriter.value((Boolean) value);
+                } else if (value instanceof CompletionInfo[]) {
+                    CompletionInfo[] ci = (CompletionInfo[]) value;
+                    mJsonWriter.beginArray();
+                    for (int j = 0; j < ci.length; j++) {
+                        mJsonWriter.value(ci[j].toString());
+                    }
+                    mJsonWriter.endArray();
+                } else if (value instanceof SharedPreferences) {
+                    SharedPreferences prefs = (SharedPreferences) value;
+                    mJsonWriter.beginObject();
+                    for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
+                        mJsonWriter.name(entry.getKey());
+                        final Object innerValue = entry.getValue();
+                        if (innerValue == null) {
+                            mJsonWriter.nullValue();
+                        } else if (innerValue instanceof Boolean) {
+                            mJsonWriter.value((Boolean) innerValue);
+                        } else if (innerValue instanceof Number) {
+                            mJsonWriter.value((Number) innerValue);
+                        } else {
+                            mJsonWriter.value(innerValue.toString());
+                        }
+                    }
+                    mJsonWriter.endObject();
+                } else if (value instanceof Key[]) {
+                    Key[] keyboardKeys = (Key[]) value;
+                    mJsonWriter.beginArray();
+                    for (Key keyboardKey : keyboardKeys) {
+                        mJsonWriter.beginObject();
+                        mJsonWriter.name("code").value(keyboardKey.mCode);
+                        mJsonWriter.name("altCode").value(keyboardKey.mAltCode);
+                        mJsonWriter.name("x").value(keyboardKey.mX);
+                        mJsonWriter.name("y").value(keyboardKey.mY);
+                        mJsonWriter.name("w").value(keyboardKey.mWidth);
+                        mJsonWriter.name("h").value(keyboardKey.mHeight);
+                        mJsonWriter.endObject();
+                    }
+                    mJsonWriter.endArray();
+                } else if (value instanceof SuggestedWords) {
+                    SuggestedWords words = (SuggestedWords) value;
+                    mJsonWriter.beginObject();
+                    mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
+                    mJsonWriter.name("hasAutoCorrectionCandidate")
+                        .value(words.mHasAutoCorrectionCandidate);
+                    mJsonWriter.name("isPunctuationSuggestions")
+                        .value(words.mIsPunctuationSuggestions);
+                    mJsonWriter.name("allowsToBeAutoCorrected")
+                        .value(words.mAllowsToBeAutoCorrected);
+                    mJsonWriter.name("isObsoleteSuggestions")
+                        .value(words.mIsObsoleteSuggestions);
+                    mJsonWriter.name("isPrediction")
+                        .value(words.mIsPrediction);
+                    mJsonWriter.name("words");
+                    mJsonWriter.beginArray();
+                    final int size = words.size();
+                    for (int j = 0; j < size; j++) {
+                        SuggestedWordInfo wordInfo = words.getWordInfo(j);
+                        mJsonWriter.value(wordInfo.toString());
+                    }
+                    mJsonWriter.endArray();
+                    mJsonWriter.endObject();
+                } else if (value == null) {
+                    mJsonWriter.nullValue();
+                } else {
+                    Log.w(TAG, "Unrecognized type to be logged: " +
+                            (value == null ? "<null>" : value.getClass().getName()));
+                    mJsonWriter.nullValue();
+                }
+            }
+            mJsonWriter.endObject();
+        } catch (IOException e) {
+            e.printStackTrace();
+            Log.w(TAG, "Error in JsonWriter; disabling logging");
+            try {
+                mJsonWriter.close();
+            } catch (IllegalStateException e1) {
+                // assume that this is just the json not being terminated properly.
+                // ignore
+            } catch (IOException e1) {
+                e1.printStackTrace();
+            } finally {
+                mJsonWriter = NULL_JSON_WRITER;
+            }
+        }
+    }
+
+    private static class LogUnit implements Runnable {
+        private final List<String[]> mKeysList = new ArrayList<String[]>();
+        private final List<Object[]> mValuesList = new ArrayList<Object[]>();
+        private final List<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>();
+        private boolean mRemovePotentiallyPrivateEvents = true;
+
+        private void addLogAtom(final String[] keys, final Object[] values,
+                final Boolean isPotentiallyPrivate) {
+            mKeysList.add(keys);
+            mValuesList.add(values);
+            mIsPotentiallyPrivate.add(isPotentiallyPrivate);
+        }
+
+        void setRemovePotentiallyPrivateEvents(boolean removePotentiallyPrivateEvents) {
+            mRemovePotentiallyPrivateEvents = removePotentiallyPrivateEvents;
+        }
+
+        @Override
+        public void run() {
+            final int numAtoms = mKeysList.size();
+            for (int atomIndex = 0; atomIndex < numAtoms; atomIndex++) {
+                if (mRemovePotentiallyPrivateEvents && mIsPotentiallyPrivate.get(atomIndex)) {
+                    continue;
+                }
+                final String[] keys = mKeysList.get(atomIndex);
+                final Object[] values = mValuesList.get(atomIndex);
+                ResearchLogger.getInstance().outputEvent(keys, values);
+            }
+        }
+    }
+
+    private static int scrubDigitFromCodePoint(int codePoint) {
+        return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
+    }
+
+    /* package for test */ static String scrubDigitsFromString(String s) {
+        StringBuilder sb = null;
+        final int length = s.length();
+        for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
+            final int codePoint = Character.codePointAt(s, i);
+            if (Character.isDigit(codePoint)) {
+                if (sb == null) {
+                    sb = new StringBuilder(length);
+                    sb.append(s.substring(0, i));
+                }
+                sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
+            } else {
+                if (sb != null) {
+                    sb.appendCodePoint(codePoint);
+                }
+            }
+        }
+        if (sb == null) {
+            return s;
+        } else {
+            return sb.toString();
+        }
+    }
+
+    private String scrubWord(String word) {
+        if (mDictionary == null) {
+            return WORD_REPLACEMENT_STRING;
+        }
+        if (mDictionary.isValidWord(word)) {
+            return word;
+        }
+        return WORD_REPLACEMENT_STRING;
+    }
+
+    private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
+        "LatinKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size",
+        "pressure"
+    };
+    public static void latinKeyboardView_processMotionEvent(final MotionEvent me, final int action,
+            final long eventTime, final int index, final int id, final int x, final int y) {
+        if (me != null) {
+            final String actionString;
+            switch (action) {
+                case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
+                case MotionEvent.ACTION_UP: actionString = "UP"; break;
+                case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
+                case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
+                case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
+                case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
+                case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
+                default: actionString = "ACTION_" + action; break;
+            }
+            final float size = me.getSize(index);
+            final float pressure = me.getPressure(index);
+            final Object[] values = {
+                actionString, eventTime, id, x, y, size, pressure
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT, values);
+        }
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONCODEINPUT = {
+        "LatinIMEOnCodeInput", "code", "x", "y"
+    };
+    public static void latinIME_onCodeInput(final int code, final int x, final int y) {
+        final Object[] values = {
+            Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+    }
+
+    private static final String[] EVENTKEYS_CORRECTION = {
+        "LogCorrection", "subgroup", "before", "after", "position"
+    };
+    public static void logCorrection(final String subgroup, final String before, final String after,
+            final int position) {
+        final Object[] values = {
+            subgroup, scrubDigitsFromString(before), scrubDigitsFromString(after), position
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_CORRECTION, values);
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION = {
+        "LatinIMECommitCurrentAutoCorrection", "typedWord", "autoCorrection"
+    };
+    public static void latinIME_commitCurrentAutoCorrection(final String typedWord,
+            final String autoCorrection) {
+        final Object[] values = {
+            scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection)
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(
+                EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values);
+        researchLogger.flushQueue(researchLogger.isPrivacyThreat(autoCorrection));
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = {
+        "LatinIMECommitText", "typedWord"
+    };
+    public static void latinIME_commitText(final CharSequence typedWord) {
+        final String scrubbedWord = scrubDigitsFromString(typedWord.toString());
+        final Object[] values = {
+            scrubbedWord
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values);
+        researchLogger.flushQueue(researchLogger.isPrivacyThreat(scrubbedWord));
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = {
+        "LatinIMEDeleteSurroundingText", "length"
+    };
+    public static void latinIME_deleteSurroundingText(final int length) {
+        final Object[] values = {
+            length
+        };
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT, values);
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD = {
+        "LatinIMEDoubleSpaceAutoPeriod"
+    };
+    public static void latinIME_doubleSpaceAutoPeriod() {
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD, EVENTKEYS_NULLVALUES);
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
+        "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions"
+    };
+    public static void latinIME_onDisplayCompletions(
+            final CompletionInfo[] applicationSpecifiedCompletions) {
+        final Object[] values = {
+            applicationSpecifiedCompletions
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS,
+                values);
+    }
+
+    /* package */ static boolean getAndClearLatinIMEExpectingUpdateSelection() {
+        boolean returnValue = sLatinIMEExpectingUpdateSelection;
+        sLatinIMEExpectingUpdateSelection = false;
+        return returnValue;
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONWINDOWHIDDEN = {
+        "LatinIMEOnWindowHidden", "isTextTruncated", "text"
+    };
+    public static void latinIME_onWindowHidden(final int savedSelectionStart,
+            final int savedSelectionEnd, final InputConnection ic) {
+        if (ic != null) {
+            ic.beginBatchEdit();
+            ic.performContextMenuAction(android.R.id.selectAll);
+            CharSequence charSequence = ic.getSelectedText(0);
+            ic.setSelection(savedSelectionStart, savedSelectionEnd);
+            ic.endBatchEdit();
+            sLatinIMEExpectingUpdateSelection = true;
+            final Object[] values = new Object[2];
+            if (OUTPUT_ENTIRE_BUFFER) {
+                if (TextUtils.isEmpty(charSequence)) {
+                    values[0] = false;
+                    values[1] = "";
+                } else {
+                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
+                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
+                        // do not cut in the middle of a supplementary character
+                        final char c = charSequence.charAt(length - 1);
+                        if (Character.isHighSurrogate(c)) {
+                            length--;
+                        }
+                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
+                                length);
+                        values[0] = true;
+                        values[1] = truncatedCharSequence.toString();
+                    } else {
+                        values[0] = false;
+                        values[1] = charSequence.toString();
+                    }
+                }
+            } else {
+                values[0] = true;
+                values[1] = "";
+            }
+            final ResearchLogger researchLogger = getInstance();
+            researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
+            researchLogger.flushQueue(true); // Play it safe.  Remove privacy-sensitive events.
+        }
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = {
+        "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
+        "fieldId", "display", "model", "prefs", "outputFormatVersion"
+    };
     public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
             final SharedPreferences prefs) {
-        if (UnsLogGroup.LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED) {
-            final StringBuilder builder = new StringBuilder();
-            builder.append("onStartInputView: editorInfo:");
-            builder.append("\tinputType=");
-            builder.append(Integer.toHexString(editorInfo.inputType));
-            builder.append("\timeOptions=");
-            builder.append(Integer.toHexString(editorInfo.imeOptions));
-            builder.append("\tdisplay="); builder.append(Build.DISPLAY);
-            builder.append("\tmodel="); builder.append(Build.MODEL);
-            for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
-                builder.append("\t" + entry.getKey());
-                Object value = entry.getValue();
-                builder.append("=" + ((value == null) ? "<null>" : value.toString()));
-            }
-            logUnstructured("LatinIME_onStartInputViewInternal", builder.toString());
+        if (editorInfo != null) {
+            final Object[] values = {
+                getUUID(prefs), editorInfo.packageName, Integer.toHexString(editorInfo.inputType),
+                Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY,
+                Build.MODEL, prefs, OUTPUT_FORMAT_VERSION
+            };
+            getInstance().enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values);
         }
     }
 
+    private static String getUUID(final SharedPreferences prefs) {
+        String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
+        if (null == uuidString) {
+            UUID uuid = UUID.randomUUID();
+            uuidString = uuid.toString();
+            Editor editor = prefs.edit();
+            editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
+            editor.apply();
+        }
+        return uuidString;
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = {
+        "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart",
+        "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd",
+        "expectingUpdateSelection", "expectingUpdateSelectionFromLogger", "context"
+    };
     public static void latinIME_onUpdateSelection(final int lastSelectionStart,
             final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
             final int newSelStart, final int newSelEnd, final int composingSpanStart,
-            final int composingSpanEnd) {
-        if (UnsLogGroup.LATINIME_ONUPDATESELECTION_ENABLED) {
-            final String s = "onUpdateSelection: oss=" + oldSelStart
-                    + ", ose=" + oldSelEnd
-                    + ", lss=" + lastSelectionStart
-                    + ", lse=" + lastSelectionEnd
-                    + ", nss=" + newSelStart
-                    + ", nse=" + newSelEnd
-                    + ", cs=" + composingSpanStart
-                    + ", ce=" + composingSpanEnd;
-            logUnstructured("LatinIME_onUpdateSelection", s);
+            final int composingSpanEnd, final boolean expectingUpdateSelection,
+            final boolean expectingUpdateSelectionFromLogger,
+            final RichInputConnection connection) {
+        String word = "";
+        if (connection != null) {
+            Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
+            if (range != null) {
+                word = range.mWord;
+            }
         }
+        final ResearchLogger researchLogger = getInstance();
+        final String scrubbedWord = researchLogger.scrubWord(word);
+        final Object[] values = {
+            lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart,
+            newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection,
+            expectingUpdateSelectionFromLogger, scrubbedWord
+        };
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
     }
 
+    private static final String[] EVENTKEYS_LATINIME_PERFORMEDITORACTION = {
+        "LatinIMEPerformEditorAction", "imeActionNext"
+    };
     public static void latinIME_performEditorAction(final int imeActionNext) {
-        if (UnsLogGroup.LATINIME_PERFORMEDITORACTION_ENABLED) {
-            logUnstructured("LatinIME_performEditorAction", String.valueOf(imeActionNext));
-        }
+        final Object[] values = {
+            imeActionNext
+        };
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_PERFORMEDITORACTION, values);
     }
 
+    private static final String[] EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION = {
+        "LatinIMEPickApplicationSpecifiedCompletion", "index", "text", "x", "y"
+    };
     public static void latinIME_pickApplicationSpecifiedCompletion(final int index,
-            final CharSequence text, int x, int y) {
-        if (UnsLogGroup.LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED) {
-            final String s = String.valueOf(index) + '\t' + text + '\t' + x + '\t' + y;
-            logUnstructured("LatinIME_pickApplicationSpecifiedCompletion", s);
-        }
+            final CharSequence cs, int x, int y) {
+        final Object[] values = {
+            index, cs, x, y
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(
+                EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values);
+        researchLogger.flushQueue(researchLogger.isPrivacyThreat(cs.toString()));
     }
 
+    private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = {
+        "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y"
+    };
     public static void latinIME_pickSuggestionManually(final String replacedWord,
             final int index, CharSequence suggestion, int x, int y) {
-        if (UnsLogGroup.LATINIME_PICKSUGGESTIONMANUALLY_ENABLED) {
-            final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
-            logUnstructured("LatinIME_pickSuggestionManually", s);
-        }
+        final Object[] values = {
+            scrubDigitsFromString(replacedWord), index, suggestion == null ? null :
+                    scrubDigitsFromString(suggestion.toString()), x, y
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY,
+                values);
+        researchLogger.flushQueue(researchLogger.isPrivacyThreat(suggestion.toString()));
     }
 
+    private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = {
+        "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y"
+    };
     public static void latinIME_punctuationSuggestion(final int index,
             final CharSequence suggestion, int x, int y) {
-        if (UnsLogGroup.LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED) {
-            final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
-            logUnstructured("LatinIME_pickPunctuationSuggestion", s);
-        }
+        final Object[] values = {
+            index, suggestion, x, y
+        };
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
     }
 
+    private static final String[] EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT = {
+        "LatinIMERevertDoubleSpaceWhileInBatchEdit"
+    };
     public static void latinIME_revertDoubleSpaceWhileInBatchEdit() {
-        if (UnsLogGroup.LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED) {
-            logUnstructured("LatinIME_revertDoubleSpaceWhileInBatchEdit", "");
-        }
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT,
+                EVENTKEYS_NULLVALUES);
     }
 
+    private static final String[] EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION = {
+        "LatinIMERevertSwapPunctuation"
+    };
     public static void latinIME_revertSwapPunctuation() {
-        if (UnsLogGroup.LATINIME_REVERTSWAPPUNCTUATION_ENABLED) {
-            logUnstructured("LatinIME_revertSwapPunctuation", "");
-        }
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES);
     }
 
+    private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = {
+        "LatinIMESendKeyCodePoint", "code"
+    };
     public static void latinIME_sendKeyCodePoint(final int code) {
-        if (UnsLogGroup.LATINIME_SENDKEYCODEPOINT_ENABLED) {
-            logUnstructured("LatinIME_sendKeyCodePoint", String.valueOf(code));
-        }
+        final Object[] values = {
+            Keyboard.printableCode(scrubDigitFromCodePoint(code))
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
     }
 
+    private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = {
+        "LatinIMESwapSwapperAndSpaceWhileInBatchEdit"
+    };
     public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() {
-        if (UnsLogGroup.LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED) {
-            logUnstructured("latinIME_swapSwapperAndSpaceWhileInBatchEdit", "");
-        }
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT,
+                EVENTKEYS_NULLVALUES);
     }
 
+    private static final String[] EVENTKEYS_LATINIME_SWITCHTOKEYBOARDVIEW = {
+        "LatinIMESwitchToKeyboardView"
+    };
     public static void latinIME_switchToKeyboardView() {
-        if (UnsLogGroup.LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED) {
-            final String s = "Switch to keyboard view.";
-            logUnstructured("LatinIME_switchToKeyboardView", s);
-        }
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWITCHTOKEYBOARDVIEW, EVENTKEYS_NULLVALUES);
     }
 
+    private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_ONLONGPRESS = {
+        "LatinKeyboardViewOnLongPress"
+    };
     public static void latinKeyboardView_onLongPress() {
-        if (UnsLogGroup.LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED) {
-            final String s = "long press detected";
-            logUnstructured("LatinKeyboardView_onLongPress", s);
-        }
+        getInstance().enqueueEvent(EVENTKEYS_LATINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES);
     }
 
-    public static void latinKeyboardView_processMotionEvent(MotionEvent me, int action,
-            long eventTime, int index, int id, int x, int y) {
-        if (UnsLogGroup.LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED) {
-            final float size = me.getSize(index);
-            final float pressure = me.getPressure(index);
-            if (action != MotionEvent.ACTION_MOVE) {
-                getInstance().logMotionEvent(action, eventTime, id, x, y, size, pressure);
-            }
-        }
-    }
-
+    private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_SETKEYBOARD = {
+        "LatinKeyboardViewSetKeyboard", "elementId", "locale", "orientation", "width",
+        "modeName", "action", "navigateNext", "navigatePrevious", "clobberSettingsKey",
+        "passwordInput", "shortcutKeyEnabled", "hasShortcutKey", "languageSwitchKeyEnabled",
+        "isMultiLine", "tw", "th", "keys"
+    };
     public static void latinKeyboardView_setKeyboard(final Keyboard keyboard) {
-        if (UnsLogGroup.LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED) {
-            StringBuilder builder = new StringBuilder();
-            builder.append("id=");
-            builder.append(keyboard.mId);
-            builder.append("\tw=");
-            builder.append(keyboard.mOccupiedWidth);
-            builder.append("\th=");
-            builder.append(keyboard.mOccupiedHeight);
-            builder.append("\tkeys=[");
-            boolean first = true;
-            for (Key key : keyboard.mKeys) {
-                if (first) {
-                    first = false;
-                } else {
-                    builder.append(",");
-                }
-                builder.append("{code:");
-                builder.append(key.mCode);
-                builder.append(",altCode:");
-                builder.append(key.mAltCode);
-                builder.append(",x:");
-                builder.append(key.mX);
-                builder.append(",y:");
-                builder.append(key.mY);
-                builder.append(",w:");
-                builder.append(key.mWidth);
-                builder.append(",h:");
-                builder.append(key.mHeight);
-                builder.append("}");
-            }
-            builder.append("]");
-            logUnstructured("LatinKeyboardView_setKeyboard", builder.toString());
+        if (keyboard != null) {
+            final KeyboardId kid = keyboard.mId;
+            final boolean isPasswordView = kid.passwordInput();
+            final Object[] values = {
+                    KeyboardId.elementIdToName(kid.mElementId),
+                    kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
+                    kid.mOrientation,
+                    kid.mWidth,
+                    KeyboardId.modeName(kid.mMode),
+                    kid.imeAction(),
+                    kid.navigateNext(),
+                    kid.navigatePrevious(),
+                    kid.mClobberSettingsKey,
+                    isPasswordView,
+                    kid.mShortcutKeyEnabled,
+                    kid.mHasShortcutKey,
+                    kid.mLanguageSwitchKeyEnabled,
+                    kid.isMultiLine(),
+                    keyboard.mOccupiedWidth,
+                    keyboard.mOccupiedHeight,
+                    keyboard.mKeys
+                };
+            getInstance().enqueueEvent(EVENTKEYS_LATINKEYBOARDVIEW_SETKEYBOARD, values);
+            getInstance().setIsPasswordView(isPasswordView);
         }
     }
 
+    private static final String[] EVENTKEYS_LATINIME_REVERTCOMMIT = {
+        "LatinIMERevertCommit", "originallyTypedWord"
+    };
     public static void latinIME_revertCommit(final String originallyTypedWord) {
-        if (UnsLogGroup.LATINIME_REVERTCOMMIT_ENABLED) {
-            logUnstructured("LatinIME_revertCommit", originallyTypedWord);
-        }
+        final Object[] values = {
+            originallyTypedWord
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values);
     }
 
+    private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = {
+        "PointerTrackerCallListenerOnCancelInput"
+    };
     public static void pointerTracker_callListenerOnCancelInput() {
-        final String s = "onCancelInput";
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED) {
-            logUnstructured("PointerTracker_callListenerOnCancelInput", s);
-        }
+        getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT,
+                EVENTKEYS_NULLVALUES);
     }
 
+    private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT = {
+        "PointerTrackerCallListenerOnCodeInput", "code", "outputText", "x", "y",
+        "ignoreModifierKey", "altersCode", "isEnabled"
+    };
     public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
             final int y, final boolean ignoreModifierKey, final boolean altersCode,
             final int code) {
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED) {
-            final String s = "onCodeInput: " + Keyboard.printableCode(code)
-                    + " text=" + key.mOutputText + " x=" + x + " y=" + y
-                    + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
-                    + " enabled=" + key.isEnabled();
-            logUnstructured("PointerTracker_callListenerOnCodeInput", s);
+        if (key != null) {
+            CharSequence outputText = key.mOutputText;
+            final Object[] values = {
+                Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null
+                        : scrubDigitsFromString(outputText.toString()),
+                x, y, ignoreModifierKey, altersCode, key.isEnabled()
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values);
         }
     }
 
-    public static void pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(
-            final Key key, final boolean ignoreModifierKey) {
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED) {
-            final String s = "onPress    : " + KeyDetector.printableCode(key)
-                    + " ignoreModifier=" + ignoreModifierKey
-                    + " enabled=" + key.isEnabled();
-            logUnstructured("PointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange", s);
-        }
-    }
-
+    private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = {
+        "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey",
+        "isEnabled"
+    };
     public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
             final boolean withSliding, final boolean ignoreModifierKey) {
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED) {
-            final String s = "onRelease  : " + Keyboard.printableCode(primaryCode)
-                    + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey
-                    + " enabled="+ key.isEnabled();
-            logUnstructured("PointerTracker_callListenerOnRelease", s);
+        if (key != null) {
+            final Object[] values = {
+                Keyboard.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
+                ignoreModifierKey, key.isEnabled()
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values);
         }
     }
 
+    private static final String[] EVENTKEYS_POINTERTRACKER_ONDOWNEVENT = {
+        "PointerTrackerOnDownEvent", "deltaT", "distanceSquared"
+    };
     public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
-        if (UnsLogGroup.POINTERTRACKER_ONDOWNEVENT_ENABLED) {
-            final String s = "onDownEvent: ignore potential noise: time=" + deltaT
-                    + " distance=" + distanceSquared;
-            logUnstructured("PointerTracker_onDownEvent", s);
-        }
+        final Object[] values = {
+            deltaT, distanceSquared
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values);
     }
 
+    private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = {
+        "PointerTrackerOnMoveEvent", "x", "y", "lastX", "lastY"
+    };
     public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
             final int lastY) {
-        if (UnsLogGroup.POINTERTRACKER_ONMOVEEVENT_ENABLED) {
-            final String s = String.format("onMoveEvent: sudden move is translated to "
-                    + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y);
-            logUnstructured("PointerTracker_onMoveEvent", s);
-        }
+        final Object[] values = {
+            x, y, lastX, lastY
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values);
     }
 
+    private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
+        "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent"
+    };
     public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
-        if (UnsLogGroup.SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED) {
-            final String s = "onTouchEvent: ignore sudden jump " + me;
-            logUnstructured("SuddenJumpingTouchEventHandler_onTouchEvent", s);
+        if (me != null) {
+            final Object[] values = {
+                me.toString()
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values);
         }
     }
 
-    public static void suggestionsView_setSuggestions(final SuggestedWords mSuggestedWords) {
-        if (UnsLogGroup.SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED) {
-            logUnstructured("SuggestionsView_setSuggestions", mSuggestedWords.toString());
+    private static final String[] EVENTKEYS_SUGGESTIONSVIEW_SETSUGGESTIONS = {
+        "SuggestionsViewSetSuggestions", "suggestedWords"
+    };
+    public static void suggestionsView_setSuggestions(final SuggestedWords suggestedWords) {
+        if (suggestedWords != null) {
+            final Object[] values = {
+                suggestedWords
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_SUGGESTIONSVIEW_SETSUGGESTIONS,
+                    values);
         }
     }
-}
\ No newline at end of file
+
+    private static final String[] EVENTKEYS_USER_TIMESTAMP = {
+        "UserTimestamp"
+    };
+    public void userTimestamp() {
+        getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
new file mode 100644
index 0000000..0c19bed
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2012 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.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.util.regex.Pattern;
+
+/**
+ * Wrapper for InputConnection to simplify interaction
+ */
+public class RichInputConnection {
+    private static final String TAG = RichInputConnection.class.getSimpleName();
+    private static final boolean DBG = false;
+    // Provision for a long word pair and a separator
+    private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
+    private static final Pattern spaceRegex = Pattern.compile("\\s+");
+    private static final int INVALID_CURSOR_POSITION = -1;
+
+    InputConnection mIC;
+    int mNestLevel;
+    public RichInputConnection() {
+        mIC = null;
+        mNestLevel = 0;
+    }
+
+    public void beginBatchEdit(final InputConnection newInputConnection) {
+        if (++mNestLevel == 1) {
+            mIC = newInputConnection;
+            if (null != mIC) mIC.beginBatchEdit();
+        } else {
+            if (DBG) {
+                throw new RuntimeException("Nest level too deep");
+            } else {
+                Log.e(TAG, "Nest level too deep : " + mNestLevel);
+            }
+        }
+    }
+    public void endBatchEdit() {
+        if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
+        if (--mNestLevel == 0 && null != mIC) mIC.endBatchEdit();
+    }
+
+    private void checkBatchEdit() {
+        if (mNestLevel != 1) {
+            // TODO: exception instead
+            Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
+            Log.e(TAG, Utils.getStackTrace(4));
+        }
+    }
+
+    public void finishComposingText() {
+        checkBatchEdit();
+        if (null != mIC) mIC.finishComposingText();
+    }
+
+    public void commitText(final CharSequence text, final int i) {
+        checkBatchEdit();
+        if (null != mIC) mIC.commitText(text, i);
+    }
+
+    public int getCursorCapsMode(final int inputType) {
+        if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
+        return mIC.getCursorCapsMode(inputType);
+    }
+
+    public CharSequence getTextBeforeCursor(final int i, final int j) {
+        if (null != mIC) return mIC.getTextBeforeCursor(i, j);
+        return null;
+    }
+
+    public CharSequence getTextAfterCursor(final int i, final int j) {
+        if (null != mIC) return mIC.getTextAfterCursor(i, j);
+        return null;
+    }
+
+    public void deleteSurroundingText(final int i, final int j) {
+        checkBatchEdit();
+        if (null != mIC) mIC.deleteSurroundingText(i, j);
+    }
+
+    public void performEditorAction(final int actionId) {
+        if (null != mIC) mIC.performEditorAction(actionId);
+    }
+
+    public void sendKeyEvent(final KeyEvent keyEvent) {
+        checkBatchEdit();
+        if (null != mIC) mIC.sendKeyEvent(keyEvent);
+    }
+
+    public void setComposingText(final CharSequence text, final int i) {
+        checkBatchEdit();
+        if (null != mIC) mIC.setComposingText(text, i);
+    }
+
+    public void setSelection(final int from, final int to) {
+        checkBatchEdit();
+        if (null != mIC) mIC.setSelection(from, to);
+    }
+
+    public void commitCorrection(final CorrectionInfo correctionInfo) {
+        checkBatchEdit();
+        if (null != mIC) mIC.commitCorrection(correctionInfo);
+    }
+
+    public void commitCompletion(final CompletionInfo completionInfo) {
+        checkBatchEdit();
+        if (null != mIC) mIC.commitCompletion(completionInfo);
+    }
+
+    public CharSequence getPreviousWord(final String sentenceSeperators) {
+        //TODO: Should fix this. This could be slow!
+        if (null == mIC) return null;
+        CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+        return getPreviousWord(prev, sentenceSeperators);
+    }
+
+    /**
+     * Represents a range of text, relative to the current cursor position.
+     */
+    public static class Range {
+        /** Characters before selection start */
+        public final int mCharsBefore;
+
+        /**
+         * Characters after selection start, including one trailing word
+         * separator.
+         */
+        public final int mCharsAfter;
+
+        /** The actual characters that make up a word */
+        public final String mWord;
+
+        public Range(int charsBefore, int charsAfter, String word) {
+            if (charsBefore < 0 || charsAfter < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+            this.mCharsBefore = charsBefore;
+            this.mCharsAfter = charsAfter;
+            this.mWord = word;
+        }
+    }
+
+    private static boolean isSeparator(int code, String sep) {
+        return sep.indexOf(code) != -1;
+    }
+
+    // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
+    // Also, it won't return words that end in a separator.
+    // Example :
+    // "abc def|" -> abc
+    // "abc def |" -> abc
+    // "abc def. |" -> abc
+    // "abc def . |" -> def
+    // "abc|" -> null
+    // "abc |" -> null
+    // "abc. def|" -> null
+    public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
+        if (prev == null) return null;
+        String[] w = spaceRegex.split(prev);
+
+        // If we can't find two words, or we found an empty word, return null.
+        if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
+
+        // If ends in a separator, return null
+        char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
+        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+        return w[w.length - 2];
+    }
+
+    public CharSequence getThisWord(String sentenceSeperators) {
+        if (null == mIC) return null;
+        final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+        return getThisWord(prev, sentenceSeperators);
+    }
+
+    // Get the word immediately before the cursor, even if there is whitespace between it and
+    // the cursor - but not if there is punctuation.
+    // Example :
+    // "abc def|" -> def
+    // "abc def |" -> def
+    // "abc def. |" -> null
+    // "abc def . |" -> null
+    public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
+        if (prev == null) return null;
+        String[] w = spaceRegex.split(prev);
+
+        // No word : return null
+        if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
+
+        // If ends in a separator, return null
+        char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
+        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+        return w[w.length - 1];
+    }
+
+    /**
+     * @param separators characters which may separate words
+     * @return the word that surrounds the cursor, including up to one trailing
+     *   separator. For example, if the field contains "he|llo world", where |
+     *   represents the cursor, then "hello " will be returned.
+     */
+    public String getWordAtCursor(String separators) {
+        // getWordRangeAtCursor returns null if the connection is null
+        Range r = getWordRangeAtCursor(separators, 0);
+        return (r == null) ? null : r.mWord;
+    }
+
+    private int getCursorPosition() {
+        if (null == mIC) return INVALID_CURSOR_POSITION;
+        final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0);
+        if (extracted == null) {
+            return INVALID_CURSOR_POSITION;
+        }
+        return extracted.startOffset + extracted.selectionStart;
+    }
+
+    /**
+     * Returns the text surrounding the cursor.
+     *
+     * @param sep a string of characters that split words.
+     * @param additionalPrecedingWordsCount the number of words before the current word that should
+     *   be included in the returned range
+     * @return a range containing the text surrounding the cursor
+     */
+    public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) {
+        if (mIC == null || sep == null) {
+            return null;
+        }
+        CharSequence before = mIC.getTextBeforeCursor(1000, 0);
+        CharSequence after = mIC.getTextAfterCursor(1000, 0);
+        if (before == null || after == null) {
+            return null;
+        }
+
+        // Going backward, alternate skipping non-separators and separators until enough words
+        // have been read.
+        int start = before.length();
+        boolean isStoppingAtWhitespace = true;  // toggles to indicate what to stop at
+        while (true) { // see comments below for why this is guaranteed to halt
+            while (start > 0) {
+                final int codePoint = Character.codePointBefore(before, start);
+                if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+                    break;  // inner loop
+                }
+                --start;
+                if (Character.isSupplementaryCodePoint(codePoint)) {
+                    --start;
+                }
+            }
+            // isStoppingAtWhitespace is true every other time through the loop,
+            // so additionalPrecedingWordsCount is guaranteed to become < 0, which
+            // guarantees outer loop termination
+            if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) {
+                break;  // outer loop
+            }
+            isStoppingAtWhitespace = !isStoppingAtWhitespace;
+        }
+
+        // Find last word separator after the cursor
+        int end = -1;
+        while (++end < after.length()) {
+            final int codePoint = Character.codePointAt(after, end);
+            if (isSeparator(codePoint, sep)) {
+                break;
+            }
+            if (Character.isSupplementaryCodePoint(codePoint)) {
+                ++end;
+            }
+        }
+
+        int cursor = getCursorPosition();
+        if (start >= 0 && cursor + end <= after.length() + before.length()) {
+            String word = before.toString().substring(start, before.length())
+                    + after.toString().substring(0, end);
+            return new Range(before.length() - start, end, word);
+        }
+
+        return null;
+    }
+
+    public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
+        CharSequence before = getTextBeforeCursor(1, 0);
+        CharSequence after = getTextAfterCursor(1, 0);
+        if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
+                && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
+            return true;
+        }
+        if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
+                && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
+            return true;
+        }
+        return false;
+    }
+
+    public void removeTrailingSpace() {
+        checkBatchEdit();
+        final CharSequence lastOne = getTextBeforeCursor(1, 0);
+        if (lastOne != null && lastOne.length() == 1
+                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
+            deleteSurroundingText(1, 0);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.latinIME_deleteSurroundingText(1);
+            }
+        }
+    }
+
+    public boolean sameAsTextBeforeCursor(final CharSequence text) {
+        final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
+        return TextUtils.equals(text, beforeText);
+    }
+
+    /* (non-javadoc)
+     * Returns the word before the cursor if the cursor is at the end of a word, null otherwise
+     */
+    public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
+        // Bail out if the cursor is not at the end of a word (cursor must be preceded by
+        // non-whitespace, non-separator, non-start-of-text)
+        // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
+        final CharSequence textBeforeCursor = getTextBeforeCursor(1, 0);
+        if (TextUtils.isEmpty(textBeforeCursor)
+                || settings.isWordSeparator(textBeforeCursor.charAt(0))) return null;
+
+        // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
+        // separator or end of line/text)
+        // Example: "test|"<EOL> "te|st" get rejected here
+        final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
+        if (!TextUtils.isEmpty(textAfterCursor)
+                && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
+
+        // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
+        // Example: " -|" gets rejected here but "e-|" and "e|" are okay
+        CharSequence word = getWordAtCursor(settings.mWordSeparators);
+        // We don't suggest on leading single quotes, so we have to remove them from the word if
+        // it starts with single quotes.
+        while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
+            word = word.subSequence(1, word.length());
+        }
+        if (TextUtils.isEmpty(word)) return null;
+        final char firstChar = word.charAt(0); // we just tested that word is not empty
+        if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
+
+        // We only suggest on words that start with a letter or a symbol that is excluded from
+        // word separators (see #handleCharacterWhileInBatchEdit).
+        if (!(Character.isLetter(firstChar)
+                || settings.isSymbolExcludedFromWordSeparators(firstChar))) {
+            return null;
+        }
+
+        return word;
+    }
+
+    public boolean revertDoubleSpace() {
+        checkBatchEdit();
+        // Here we test whether we indeed have a period and a space before us. This should not
+        // be needed, but it's there just in case something went wrong.
+        final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+        if (!". ".equals(textBeforeCursor)) {
+            // Theoretically we should not be coming here if there isn't ". " before the
+            // cursor, but the application may be changing the text while we are typing, so
+            // anything goes. We should not crash.
+            Log.d(TAG, "Tried to revert double-space combo but we didn't find "
+                    + "\". \" just before the cursor.");
+            return false;
+        }
+        deleteSurroundingText(2, 0);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_deleteSurroundingText(2);
+        }
+        commitText("  ", 1);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
+        }
+        return true;
+    }
+
+    public boolean revertSwapPunctuation() {
+        checkBatchEdit();
+        // Here we test whether we indeed have a space and something else before us. This should not
+        // be needed, but it's there just in case something went wrong.
+        final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
+        // enter surrogate pairs this code will have been removed.
+        if (TextUtils.isEmpty(textBeforeCursor)
+                || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
+            // We may only come here if the application is changing the text while we are typing.
+            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
+            // but some debugging log may be in order.
+            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
+                    + "find a space just before the cursor.");
+            return false;
+        }
+        deleteSurroundingText(2, 0);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_deleteSurroundingText(2);
+        }
+        commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_revertSwapPunctuation();
+        }
+        return true;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 152d668..4c67b49 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -69,9 +69,7 @@
     public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
             "pref_key_preview_popup_dismiss_delay";
     public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
-    public static final String PREF_BIGRAM_SUGGESTION = "next_word_suggestion";
     public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
-    public static final String PREF_KEY_ENABLE_SPAN_INSERT = "enable_span_insert";
     public static final String PREF_VIBRATION_DURATION_SETTINGS =
             "pref_vibration_duration_settings";
     public static final String PREF_KEYPRESS_SOUND_VOLUME =
@@ -87,9 +85,7 @@
     private ListPreference mShowCorrectionSuggestionsPreference;
     private ListPreference mAutoCorrectionThresholdPreference;
     private ListPreference mKeyPreviewPopupDismissDelay;
-    // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
-    private CheckBoxPreference mBigramSuggestion;
-    // Prediction: use bigrams to predict the next word when there is no input for it yet
+    // Use bigrams to predict the next word when there is no input for it yet
     private CheckBoxPreference mBigramPrediction;
     private Preference mDebugSettingsPreference;
 
@@ -100,7 +96,6 @@
         final String autoCorrectionOff = getResources().getString(
                 R.string.auto_correction_threshold_mode_index_off);
         final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
-        mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff));
         if (null != mBigramPrediction) {
             mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
         }
@@ -124,7 +119,6 @@
 
         mAutoCorrectionThresholdPreference =
                 (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
-        mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTION);
         mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS);
         mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
         if (mDebugSettingsPreference != null) {
@@ -149,12 +143,9 @@
             generalSettings.removePreference(mVoicePreference);
         }
 
-        final PreferenceGroup advancedSettings =
-                (PreferenceGroup) findPreference(PREF_ADVANCED_SETTINGS);
-        // Remove those meaningless options for now. TODO: delete them for good
-        advancedSettings.removePreference(findPreference(PREF_BIGRAM_SUGGESTION));
-        advancedSettings.removePreference(findPreference(PREF_KEY_ENABLE_SPAN_INSERT));
         if (!VibratorUtils.getInstance(context).hasVibrator()) {
+            final PreferenceGroup advancedSettings =
+                    (PreferenceGroup) findPreference(PREF_ADVANCED_SETTINGS);
             generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
             if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
                 advancedSettings.removePreference(findPreference(PREF_VIBRATION_DURATION_SETTINGS));
@@ -167,15 +158,6 @@
             generalSettings.removePreference(findPreference(PREF_POPUP_ON));
         }
 
-        final boolean showBigramSuggestionsOption = res.getBoolean(
-                R.bool.config_enable_next_word_suggestions_option);
-        if (!showBigramSuggestionsOption) {
-            textCorrectionGroup.removePreference(mBigramSuggestion);
-            if (null != mBigramPrediction) {
-                textCorrectionGroup.removePreference(mBigramPrediction);
-            }
-        }
-
         final CheckBoxPreference includeOtherImesInLanguageSwitchList =
                 (CheckBoxPreference)findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
         includeOtherImesInLanguageSwitchList.setEnabled(
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index b07c3e5..ef423f1 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.util.Log;
 import android.view.inputmethod.EditorInfo;
@@ -29,7 +30,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Map;
 
 /**
  * When you call the constructor of this class, you may want to change the current system locale by
@@ -38,6 +38,19 @@
 public class SettingsValues {
     private static final String TAG = SettingsValues.class.getSimpleName();
 
+    private static final int SUGGESTION_VISIBILITY_SHOW_VALUE
+            = R.string.prefs_suggestion_visibility_show_value;
+    private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
+    private static final int SUGGESTION_VISIBILITY_HIDE_VALUE
+            = R.string.prefs_suggestion_visibility_hide_value;
+
+    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
+        SUGGESTION_VISIBILITY_SHOW_VALUE,
+        SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE,
+        SUGGESTION_VISIBILITY_HIDE_VALUE
+    };
+
     // From resources:
     public final int mDelayUpdateOldSuggestions;
     public final String mWeakSpaceStrippers;
@@ -63,27 +76,30 @@
     @SuppressWarnings("unused") // TODO: Use this
     private final String mKeyPreviewPopupDismissDelayRawValue;
     public final boolean mUseContactsDict;
-    // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
-    public final boolean mBigramSuggestionEnabled;
-    // Prediction: use bigrams to predict the next word when there is no input for it yet
+    // Use bigrams to predict the next word when there is no input for it yet
     public final boolean mBigramPredictionEnabled;
-    public final boolean mEnableSuggestionSpanInsertion;
     @SuppressWarnings("unused") // TODO: Use this
     private final int mVibrationDurationSettingsRawValue;
     @SuppressWarnings("unused") // TODO: Use this
     private final float mKeypressSoundVolumeRawValue;
     private final InputMethodSubtype[] mAdditionalSubtypes;
 
+    // From the input box
+    private final InputAttributes mInputAttributes;
+
     // Deduced settings
     public final int mKeypressVibrationDuration;
     public final float mFxVolume;
     public final int mKeyPreviewPopupDismissDelay;
-    public final boolean mAutoCorrectEnabled;
+    private final boolean mAutoCorrectEnabled;
     public final float mAutoCorrectionThreshold;
+    public final boolean mCorrectionEnabled;
+    public final int mSuggestionVisibility;
     private final boolean mVoiceKeyEnabled;
     private final boolean mVoiceKeyOnMain;
 
-    public SettingsValues(final SharedPreferences prefs, final Context context) {
+    public SettingsValues(final SharedPreferences prefs, final InputAttributes inputAttributes,
+            final Context context) {
         final Resources res = context.getResources();
 
         // Get the resources
@@ -109,6 +125,13 @@
                 mSymbolsExcludedFromWordSeparators, res);
         mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
 
+        // Store the input attributes
+        if (null == inputAttributes) {
+            mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+        } else {
+            mInputAttributes = inputAttributes;
+        }
+
         // Get the settings preferences
         mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
         mVibrateOn = isVibrateOn(context, prefs, res);
@@ -131,12 +154,7 @@
                 Integer.toString(res.getInteger(R.integer.config_key_preview_linger_timeout)));
         mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
         mAutoCorrectEnabled = isAutoCorrectEnabled(res, mAutoCorrectionThresholdRawValue);
-        mBigramSuggestionEnabled = mAutoCorrectEnabled
-                && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled);
-        mBigramPredictionEnabled = mBigramSuggestionEnabled
-                && isBigramPredictionEnabled(prefs, res);
-        // TODO: remove mEnableSuggestionSpanInsertion. It's always true.
-        mEnableSuggestionSpanInsertion = true;
+        mBigramPredictionEnabled = isBigramPredictionEnabled(prefs, res);
         mVibrationDurationSettingsRawValue =
                 prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
         mKeypressSoundVolumeRawValue = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
@@ -151,16 +169,17 @@
         mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain);
         mAdditionalSubtypes = AdditionalSubtype.createAdditionalSubtypesArray(
                 getPrefAdditionalSubtypes(prefs, res));
+        mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
+        mSuggestionVisibility = createSuggestionVisibility(res);
     }
 
     // Helper functions to create member values.
     private static SuggestedWords createSuggestPuncList(final String[] puncs) {
-        final ArrayList<SuggestedWords.SuggestedWordInfo> puncList =
-                new ArrayList<SuggestedWords.SuggestedWordInfo>();
+        final ArrayList<SuggestedWordInfo> puncList = new ArrayList<SuggestedWordInfo>();
         if (puncs != null) {
             for (final String puncSpec : puncs) {
-                puncList.add(new SuggestedWords.SuggestedWordInfo(
-                        KeySpecParser.getLabel(puncSpec), SuggestedWordInfo.MAX_SCORE));
+                puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
+                        SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED));
             }
         }
         return new SuggestedWords(puncList,
@@ -184,6 +203,16 @@
         return wordSeparators;
     }
 
+    private int createSuggestionVisibility(final Resources res) {
+        final String suggestionVisiblityStr = mShowSuggestionsSetting;
+        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
+            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
+                return visibility;
+            }
+        }
+        throw new RuntimeException("Bug: visibility string is not configured correctly");
+    }
+
     private static boolean isVibrateOn(final Context context, final SharedPreferences prefs,
             final Resources res) {
         final boolean hasVibrator = VibratorUtils.getInstance(context).hasVibrator();
@@ -191,6 +220,26 @@
                 res.getBoolean(R.bool.config_default_vibration_enabled));
     }
 
+    public boolean isApplicationSpecifiedCompletionsOn() {
+        return mInputAttributes.mApplicationSpecifiedCompletionOn;
+    }
+
+    public boolean isEditorActionNext() {
+        return mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT;
+    }
+
+    public boolean isSuggestionsRequested(final int displayOrientation) {
+        return mInputAttributes.mIsSettingsSuggestionStripOn
+                && (mCorrectionEnabled
+                        || isSuggestionStripVisibleInOrientation(displayOrientation));
+    }
+
+    public boolean isSuggestionStripVisibleInOrientation(final int orientation) {
+        return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE)
+                || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+                        && orientation == Configuration.ORIENTATION_PORTRAIT);
+    }
+
     public boolean isWordSeparator(int code) {
         return mWordSeparators.contains(String.valueOf((char)code));
     }
@@ -240,12 +289,6 @@
                         R.integer.config_key_preview_linger_timeout))));
     }
 
-    private static boolean isBigramSuggestionEnabled(final SharedPreferences sp,
-            final Resources resources, final boolean autoCorrectEnabled) {
-        // TODO: remove this method. Bigram suggestion is always true.
-        return true;
-    }
-
     private static boolean isBigramPredictionEnabled(final SharedPreferences sp,
             final Resources resources) {
         return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
@@ -367,4 +410,9 @@
         final String newStr = Utils.localeAndTimeHashMapToStr(map);
         prefs.edit().putString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply();
     }
+
+    // For debug.
+    public String getInputAttributesDebugString() {
+        return mInputAttributes.toString();
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 336a76f..8922454 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -39,9 +39,10 @@
 
     public static final int APPROX_MAX_WORD_LENGTH = 32;
 
+    // TODO: rename this to CORRECTION_OFF
     public static final int CORRECTION_NONE = 0;
+    // TODO: rename this to CORRECTION_ON
     public static final int CORRECTION_FULL = 1;
-    public static final int CORRECTION_FULL_BIGRAM = 2;
 
     // It seems the following values are only used for logging.
     public static final int DIC_USER_TYPED = 0;
@@ -65,15 +66,15 @@
 
     private static final boolean DBG = LatinImeLogger.sDBG;
 
-    private boolean mHasMainDictionary;
-    private Dictionary mContactsDict;
+    private Dictionary mMainDictionary;
+    private ContactsBinaryDictionary mContactsDict;
     private WhitelistDictionary mWhiteListDictionary;
     private final ConcurrentHashMap<String, Dictionary> mUnigramDictionaries =
             new ConcurrentHashMap<String, Dictionary>();
     private final ConcurrentHashMap<String, Dictionary> mBigramDictionaries =
             new ConcurrentHashMap<String, Dictionary>();
 
-    private int mPrefMaxSuggestions = 18;
+    public static final int MAX_SUGGESTIONS = 18;
 
     private static final int PREF_MAX_BIGRAMS = 60;
 
@@ -98,7 +99,7 @@
             final long startOffset, final long length, final Locale locale) {
         final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
                 startOffset, length /* useFullEditDistance */, false, locale);
-        mHasMainDictionary = null != mainDict;
+        mMainDictionary = mainDict;
         addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
         addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
         initWhitelistAndAutocorrectAndPool(context, locale);
@@ -129,15 +130,15 @@
     }
 
     public void resetMainDict(final Context context, final Locale locale) {
-        mHasMainDictionary = false;
+        mMainDictionary = null;
         new Thread("InitializeBinaryDictionary") {
             @Override
             public void run() {
                 final DictionaryCollection newMainDict =
                         DictionaryFactory.createMainDictionaryFromManager(context, locale);
-                mHasMainDictionary = null != newMainDict && !newMainDict.isEmpty();
                 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
                 addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
+                mMainDictionary = newMainDict;
             }
         }.start();
     }
@@ -145,10 +146,14 @@
     // The main dictionary could have been loaded asynchronously.  Don't cache the return value
     // of this method.
     public boolean hasMainDictionary() {
-        return mHasMainDictionary;
+        return null != mMainDictionary && mMainDictionary.isInitialized();
     }
 
-    public Dictionary getContactsDictionary() {
+    public Dictionary getMainDictionary() {
+        return mMainDictionary;
+    }
+
+    public ContactsBinaryDictionary getContactsDictionary() {
         return mContactsDict;
     }
 
@@ -164,7 +169,7 @@
      * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
      * before the main dictionary, if set. This refers to the system-managed user dictionary.
      */
-    public void setUserDictionary(Dictionary userDictionary) {
+    public void setUserDictionary(UserBinaryDictionary userDictionary) {
         addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary);
     }
 
@@ -173,13 +178,13 @@
      * the contacts dictionary by passing null to this method. In this case no contacts dictionary
      * won't be used.
      */
-    public void setContactsDictionary(Dictionary contactsDictionary) {
+    public void setContactsDictionary(ContactsBinaryDictionary contactsDictionary) {
         mContactsDict = contactsDictionary;
         addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
         addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
     }
 
-    public void setUserHistoryDictionary(Dictionary userHistoryDictionary) {
+    public void setUserHistoryDictionary(UserHistoryDictionary userHistoryDictionary) {
         addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER_HISTORY_UNIGRAM,
                 userHistoryDictionary);
         addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_HISTORY_BIGRAM,
@@ -217,7 +222,7 @@
         mIsFirstCharCapitalized = false;
         mIsAllUpperCase = false;
         mTrailingSingleQuotesCount = 0;
-        mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
+        mSuggestions = new ArrayList<SuggestedWordInfo>(MAX_SUGGESTIONS);
 
         // Treating USER_TYPED as UNIGRAM suggestion for logging now.
         LatinImeLogger.onAddSuggestedWord("", Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
@@ -228,7 +233,7 @@
         getAllBigrams(prevWordForBigram, sEmptyWordComposer);
 
         // Nothing entered: return all bigrams for the previous word
-        int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
+        int insertCount = Math.min(mBigramSuggestions.size(), MAX_SUGGESTIONS);
         for (int i = 0; i < insertCount; ++i) {
             addBigramToSuggestions(mBigramSuggestions.get(i));
         }
@@ -247,12 +252,12 @@
     // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
     public SuggestedWords getSuggestedWords(
             final WordComposer wordComposer, CharSequence prevWordForBigram,
-            final ProximityInfo proximityInfo, final int correctionMode) {
+            final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) {
         LatinImeLogger.onStartSuggestion(prevWordForBigram);
         mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
         mIsAllUpperCase = wordComposer.isAllUpperCase();
         mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
-        mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
+        mSuggestions = new ArrayList<SuggestedWordInfo>(MAX_SUGGESTIONS);
 
         final String typedWord = wordComposer.getTypedWord();
         final String consideredWord = mTrailingSingleQuotesCount > 0
@@ -262,7 +267,7 @@
         LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
         mConsideredWord = consideredWord;
 
-        if (wordComposer.size() <= 1 && (correctionMode == CORRECTION_FULL_BIGRAM)) {
+        if (wordComposer.size() <= 1 && isCorrectionEnabled) {
             // At first character typed, search only the bigrams
             mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
 
@@ -270,7 +275,7 @@
                 getAllBigrams(prevWordForBigram, wordComposer);
                 if (TextUtils.isEmpty(consideredWord)) {
                     // Nothing entered: return all bigrams for the previous word
-                    int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
+                    int insertCount = Math.min(mBigramSuggestions.size(), MAX_SUGGESTIONS);
                     for (int i = 0; i < insertCount; ++i) {
                         addBigramToSuggestions(mBigramSuggestions.get(i));
                     }
@@ -289,7 +294,7 @@
                         if (bigramSuggestionFirstChar == currentChar
                                 || bigramSuggestionFirstChar == currentCharUpper) {
                             addBigramToSuggestions(bigramSuggestion);
-                            if (++count > mPrefMaxSuggestions) break;
+                            if (++count > MAX_SUGGESTIONS) break;
                         }
                     }
                 }
@@ -319,7 +324,7 @@
                 mIsFirstCharCapitalized, mWhiteListDictionary.getWhitelistedWord(consideredWord));
 
         final boolean hasAutoCorrection;
-        if (CORRECTION_FULL == correctionMode || CORRECTION_FULL_BIGRAM == correctionMode) {
+        if (isCorrectionEnabled) {
             final CharSequence autoCorrection =
                     AutoCorrection.computeAutoCorrectionWord(mUnigramDictionaries, wordComposer,
                             mSuggestions, consideredWord, mAutoCorrectionThreshold,
@@ -335,15 +340,16 @@
                 for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
                     sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
                 }
-                mSuggestions.add(0, new SuggestedWordInfo(
-                        sb.toString(), SuggestedWordInfo.MAX_SCORE));
+                mSuggestions.add(0, new SuggestedWordInfo(sb.toString(),
+                        SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST));
             } else {
-                mSuggestions.add(0, new SuggestedWordInfo(
-                        whitelistedWord, SuggestedWordInfo.MAX_SCORE));
+                mSuggestions.add(0, new SuggestedWordInfo(whitelistedWord,
+                        SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST));
             }
         }
 
-        mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
+        mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
+                SuggestedWordInfo.KIND_TYPED));
         SuggestedWordInfo.removeDups(mSuggestions);
 
         final ArrayList<SuggestedWordInfo> suggestionsList;
@@ -365,10 +371,10 @@
         // 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.
-                && mHasMainDictionary;
+                && hasMainDictionary();
 
         boolean autoCorrectionAvailable = hasAutoCorrection;
-        if (correctionMode == CORRECTION_FULL || correctionMode == CORRECTION_FULL_BIGRAM) {
+        if (isCorrectionEnabled) {
             autoCorrectionAvailable |= !allowsToBeAutoCorrected;
         }
         // Don't auto-correct words with multiple capital letter
@@ -443,7 +449,7 @@
             prefMaxSuggestions = PREF_MAX_BIGRAMS;
         } else {
             suggestions = mSuggestions;
-            prefMaxSuggestions = mPrefMaxSuggestions;
+            prefMaxSuggestions = MAX_SUGGESTIONS;
         }
 
         int pos = 0;
@@ -495,7 +501,8 @@
         for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
             sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
         }
-        suggestions.add(pos, new SuggestedWordInfo(sb, score));
+        // TODO: figure out what type of suggestion this is
+        suggestions.add(pos, new SuggestedWordInfo(sb, score, SuggestedWordInfo.KIND_CORRECTION));
         if (suggestions.size() > prefMaxSuggestions) {
             suggestions.remove(prefMaxSuggestions);
         } else {
@@ -511,7 +518,7 @@
         for (final Dictionary dictionary : dictionaries) {
             dictionary.close();
         }
-        mHasMainDictionary = false;
+        mMainDictionary = null;
     }
 
     // TODO: Resolve the inconsistencies between the native auto correction algorithms and
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 497fd3b..1ed91fe 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -91,7 +91,8 @@
         final ArrayList<SuggestedWordInfo> result = new ArrayList<SuggestedWordInfo>();
         for (CompletionInfo info : infos) {
             if (null != info && info.getText() != null) {
-                result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE));
+                result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE,
+                        SuggestedWordInfo.KIND_APP_DEFINED));
             }
         }
         return result;
@@ -103,7 +104,8 @@
             final CharSequence typedWord, final SuggestedWords previousSuggestions) {
         final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<SuggestedWordInfo>();
         final HashSet<String> alreadySeen = new HashSet<String>();
-        suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
+        suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
+                SuggestedWordInfo.KIND_TYPED));
         alreadySeen.add(typedWord.toString());
         final int previousSize = previousSuggestions.size();
         for (int pos = 1; pos < previousSize; pos++) {
@@ -120,16 +122,25 @@
 
     public static class SuggestedWordInfo {
         public static final int MAX_SCORE = Integer.MAX_VALUE;
+        public static final int KIND_TYPED = 0; // What user typed
+        public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
+        public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
+        public static final int KIND_WHITELIST = 3; // Whitelisted word
+        public static final int KIND_BLACKLIST = 4; // Blacklisted word
+        public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
+        public static final int KIND_APP_DEFINED = 6; // Suggested by the application
         private final String mWordStr;
         public final CharSequence mWord;
         public final int mScore;
+        public final int mKind;
         public final int mCodePointCount;
         private String mDebugString = "";
 
-        public SuggestedWordInfo(final CharSequence word, final int score) {
+        public SuggestedWordInfo(final CharSequence word, final int score, final int kind) {
             mWordStr = word.toString();
             mWord = word;
             mScore = score;
+            mKind = kind;
             mCodePointCount = mWordStr.codePointCount(0, mWordStr.length());
         }
 
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
deleted file mode 100644
index a8b871c..0000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedContactsDictionary extends ContactsDictionary {
-    private boolean mClosed;
-
-    public SynchronouslyLoadedContactsDictionary(final Context context) {
-        super(context, Suggest.DIC_CONTACTS);
-        mClosed = false;
-    }
-
-    @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
-        blockingReloadDictionaryIfRequired();
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    @Override
-    public synchronized boolean isValidWord(CharSequence word) {
-        blockingReloadDictionaryIfRequired();
-        return getWordFrequency(word) > -1;
-    }
-
-    // Protect against multiple closing
-    @Override
-    public synchronized void close() {
-        // Actually with the current implementation of ContactsDictionary it's safe to close
-        // several times, so the following protection is really only for foolproofing
-        if (mClosed) return;
-        mClosed = true;
-        super.close();
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
deleted file mode 100644
index 23a49c1..0000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedUserDictionary extends UserDictionary {
-    private boolean mClosed;
-
-    public SynchronouslyLoadedUserDictionary(final Context context, final String locale) {
-        this(context, locale, false);
-    }
-
-    public SynchronouslyLoadedUserDictionary(final Context context, final String locale,
-            final boolean alsoUseMoreRestrictiveLocales) {
-        super(context, locale, alsoUseMoreRestrictiveLocales);
-    }
-
-    @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
-        blockingReloadDictionaryIfRequired();
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    @Override
-    public synchronized boolean isValidWord(CharSequence word) {
-        blockingReloadDictionaryIfRequired();
-        return super.isValidWord(word);
-    }
-
-    // Protect against multiple closing
-    @Override
-    public synchronized void close() {
-        if (mClosed) return;
-        mClosed = true;
-        super.close();
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
deleted file mode 100644
index c1efadd..0000000
--- a/java/src/com/android/inputmethod/latin/UserDictionary.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2008 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.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.provider.UserDictionary.Words;
-import android.text.TextUtils;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-import java.util.Arrays;
-
-// TODO: This class is superseded by {@link UserBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words in the user unigram dictionary.
- *
- * @deprecated Use {@link UserBinaryDictionary}.
- */
-@Deprecated
-public class UserDictionary extends ExpandableDictionary {
-
-    // TODO: use Words.SHORTCUT when it's public in the SDK
-    final static String SHORTCUT = "shortcut";
-    private static final String[] PROJECTION_QUERY = {
-        Words.WORD,
-        SHORTCUT,
-        Words.FREQUENCY,
-    };
-
-    // This is not exported by the framework so we pretty much have to write it here verbatim
-    private static final String ACTION_USER_DICTIONARY_INSERT =
-            "com.android.settings.USER_DICTIONARY_INSERT";
-
-    private ContentObserver mObserver;
-    final private String mLocale;
-    final private boolean mAlsoUseMoreRestrictiveLocales;
-
-    public UserDictionary(final Context context, final String locale) {
-        this(context, locale, false);
-    }
-
-    public UserDictionary(final Context context, final String locale,
-            final boolean alsoUseMoreRestrictiveLocales) {
-        super(context, Suggest.DIC_USER);
-        if (null == locale) throw new NullPointerException(); // Catch the error earlier
-        mLocale = locale;
-        mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
-        // Perform a managed query. The Activity will handle closing and re-querying the cursor
-        // when needed.
-        ContentResolver cres = context.getContentResolver();
-
-        mObserver = new ContentObserver(null) {
-            @Override
-            public void onChange(boolean self) {
-                setRequiresReload(true);
-            }
-        };
-        cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
-
-        loadDictionary();
-    }
-
-    @Override
-    public synchronized void close() {
-        if (mObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-            mObserver = null;
-        }
-        super.close();
-    }
-
-    @Override
-    public void loadDictionaryAsync() {
-        // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
-        // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
-        // This is correct for locale processing.
-        // For this example, we'll look at the "en_US_POSIX" case.
-        final String[] localeElements =
-                TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3);
-        final int length = localeElements.length;
-
-        final StringBuilder request = new StringBuilder("(locale is NULL)");
-        String localeSoFar = "";
-        // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
-        // and request = "(locale is NULL)"
-        for (int i = 0; i < length; ++i) {
-            // i | localeSoFar    | localeElements
-            // 0 | ""             | ["en", "US", "POSIX"]
-            // 1 | "en_"          | ["en", "US", "POSIX"]
-            // 2 | "en_US_"       | ["en", "en_US", "POSIX"]
-            localeElements[i] = localeSoFar + localeElements[i];
-            localeSoFar = localeElements[i] + "_";
-            // i | request
-            // 0 | "(locale is NULL)"
-            // 1 | "(locale is NULL) or (locale=?)"
-            // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
-            request.append(" or (locale=?)");
-        }
-        // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
-        // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
-
-        final String[] requestArguments;
-        // If length == 3, we already have all the arguments we need (common prefix is meaningless
-        // inside variants
-        if (mAlsoUseMoreRestrictiveLocales && length < 3) {
-            request.append(" or (locale like ?)");
-            // The following creates an array with one more (null) position
-            final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
-                    Arrays.copyOf(localeElements, length + 1);
-            localeElementsWithMoreRestrictiveLocalesIncluded[length] =
-                    localeElements[length - 1] + "_%";
-            requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
-            // If for example localeElements = ["en"]
-            // then requestArguments = ["en", "en_%"]
-            // and request = (locale is NULL) or (locale=?) or (locale like ?)
-            // If localeElements = ["en", "en_US"]
-            // then requestArguments = ["en", "en_US", "en_US_%"]
-        } else {
-            requestArguments = localeElements;
-        }
-        final Cursor cursor = getContext().getContentResolver()
-                .query(Words.CONTENT_URI, PROJECTION_QUERY, request.toString(),
-                        requestArguments, null);
-        try {
-            addWords(cursor);
-        } finally {
-            if (null != cursor) cursor.close();
-        }
-    }
-
-    public boolean isEnabled() {
-        final ContentResolver cr = getContext().getContentResolver();
-        final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
-        if (client != null) {
-            client.release();
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Adds a word to the user dictionary and makes it persistent.
-     *
-     * This will call upon the system interface to do the actual work through the intent
-     * readied by the system to this effect.
-     *
-     * @param word the word to add. If the word is capitalized, then the dictionary will
-     * recognize it as a capitalized word when searched.
-     * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
-     * the highest.
-     * @TODO use a higher or float range for frequency
-     */
-    public synchronized void addWordToUserDictionary(final String word, final int frequency) {
-        // Force load the dictionary here synchronously
-        if (getRequiresReload()) loadDictionaryAsync();
-        // TODO: do something for the UI. With the following, any sufficiently long word will
-        // look like it will go to the user dictionary but it won't.
-        // Safeguard against adding long words. Can cause stack overflow.
-        if (word.length() >= getMaxWordLength()) return;
-
-        // TODO: Add an argument to the intent to specify the frequency.
-        Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT);
-        intent.putExtra(Words.WORD, word);
-        intent.putExtra(Words.LOCALE, mLocale);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        getContext().startActivity(intent);
-    }
-
-    @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
-        super.getWords(codes, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    @Override
-    public synchronized boolean isValidWord(CharSequence word) {
-        return super.isValidWord(word);
-    }
-
-    private void addWords(Cursor cursor) {
-        clearDictionary();
-        if (cursor == null) return;
-        final int maxWordLength = getMaxWordLength();
-        if (cursor.moveToFirst()) {
-            final int indexWord = cursor.getColumnIndex(Words.WORD);
-            final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
-            final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
-            while (!cursor.isAfterLast()) {
-                String word = cursor.getString(indexWord);
-                String shortcut = cursor.getString(indexShortcut);
-                int frequency = cursor.getInt(indexFrequency);
-                // Safeguard against adding really long words. Stack may overflow due
-                // to recursion
-                if (word.length() < maxWordLength) {
-                    super.addWord(word, null, frequency);
-                }
-                if (null != shortcut && shortcut.length() < maxWordLength) {
-                    super.addWord(shortcut, word, frequency);
-                }
-                cursor.moveToNext();
-            }
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
index e5516dc..1de95d7 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
@@ -50,7 +50,7 @@
         }
 
         private ForgettingCurveParams(long now, boolean isValid) {
-            this((int)pushCount((byte)0, isValid), now, now, isValid);
+            this(pushCount((byte)0, isValid), now, now, isValid);
         }
 
         /** This constructor is called when the user history bigram dictionary is being restored. */
@@ -199,20 +199,20 @@
         public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
         static {
             for (int i = 0; i < FC_LEVEL_MAX; ++i) {
-                final double initialFreq;
+                final float initialFreq;
                 if (i >= 2) {
-                    initialFreq = (double)FC_FREQ_MAX;
+                    initialFreq = FC_FREQ_MAX;
                 } else if (i == 1) {
-                    initialFreq = (double)FC_FREQ_MAX / 2;
+                    initialFreq = FC_FREQ_MAX / 2;
                 } else if (i == 0) {
-                    initialFreq = (double)FC_FREQ_MAX / 4;
+                    initialFreq = FC_FREQ_MAX / 4;
                 } else {
                     continue;
                 }
                 for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
-                    final double elapsedHour = j * ELAPSED_TIME_INTERVAL_HOURS;
-                    final double freq =
-                            initialFreq * Math.pow(initialFreq, elapsedHour / HALF_LIFE_HOURS);
+                    final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
+                    final float freq = initialFreq
+                            * NativeUtils.powf(initialFreq, elapsedHours / HALF_LIFE_HOURS);
                     final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq));
                     SCORE_TABLE[i][j] = intFreq;
                 }
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index 4178955..f2d21ab 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -44,10 +44,8 @@
 import java.io.PrintWriter;
 import java.nio.channels.FileChannel;
 import java.text.SimpleDateFormat;
-import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.Map;
 
 public class Utils {
     private Utils() {
@@ -206,18 +204,24 @@
     }
 
     // Get the current stack trace
-    public static String getStackTrace() {
+    public static String getStackTrace(final int limit) {
         StringBuilder sb = new StringBuilder();
         try {
             throw new RuntimeException();
         } catch (RuntimeException e) {
             StackTraceElement[] frames = e.getStackTrace();
             // Start at 1 because the first frame is here and we don't care about it
-            for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n");
+            for (int j = 1; j < frames.length && j < limit + 1; ++j) {
+                sb.append(frames[j].toString() + "\n");
+            }
         }
         return sb.toString();
     }
 
+    public static String getStackTrace() {
+        return getStackTrace(Integer.MAX_VALUE);
+    }
+
     public static class UsabilityStudyLogUtils {
         // TODO: remove code duplication with ResearchLog class
         private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
index 89c59f8..2c3eee7 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -787,9 +787,9 @@
         // (discretizedFrequency + 0.5) times this value to get the median value of the step,
         // which is the best approximation. This is how we get the most precise result with
         // only four bits.
-        final double stepSize =
-                (double)(MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5 + MAX_BIGRAM_FREQUENCY);
-        final double firstStepStart = 1 + unigramFrequency + (stepSize / 2.0);
+        final float stepSize =
+                (MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5f + MAX_BIGRAM_FREQUENCY);
+        final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f);
         final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize);
         // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1
         // here. The best approximation would be the unigram freq itself, so we should not
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 88efc5a..7fffc31 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -30,18 +30,17 @@
 import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.ContactsBinaryDictionary;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.Dictionary.WordCallback;
 import com.android.inputmethod.latin.DictionaryCollection;
 import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.LocaleUtils;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StringUtils;
 import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary;
-import com.android.inputmethod.latin.SynchronouslyLoadedContactsDictionary;
 import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
-import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
+import com.android.inputmethod.latin.UserBinaryDictionary;
 import com.android.inputmethod.latin.WhitelistDictionary;
 import com.android.inputmethod.latin.WordComposer;
 
@@ -73,11 +72,11 @@
     private final static String[] EMPTY_STRING_ARRAY = new String[0];
     private Map<String, DictionaryPool> mDictionaryPools =
             Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
-    private Map<String, Dictionary> mUserDictionaries =
-            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+    private Map<String, UserBinaryDictionary> mUserDictionaries =
+            Collections.synchronizedMap(new TreeMap<String, UserBinaryDictionary>());
     private Map<String, Dictionary> mWhitelistDictionaries =
             Collections.synchronizedMap(new TreeMap<String, Dictionary>());
-    private Dictionary mContactsDictionary;
+    private ContactsBinaryDictionary mContactsDictionary;
 
     // The threshold for a candidate to be offered as a suggestion.
     private float mSuggestionThreshold;
@@ -154,13 +153,9 @@
 
     private void startUsingContactsDictionaryLocked() {
         if (null == mContactsDictionary) {
-            if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
-                // TODO: use the right locale for each session
-                mContactsDictionary =
-                        new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
-            } else {
-                mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
-            }
+            // TODO: use the right locale for each session
+            mContactsDictionary =
+                    new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
         }
         final Iterator<WeakReference<DictionaryCollection>> iterator =
                 mDictionaryCollectionsList.iterator();
@@ -368,8 +363,9 @@
     private void closeAllDictionaries() {
         final Map<String, DictionaryPool> oldPools = mDictionaryPools;
         mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
-        final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
-        mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+        final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries;
+        mUserDictionaries =
+                Collections.synchronizedMap(new TreeMap<String, UserBinaryDictionary>());
         final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
         mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
         new Thread("spellchecker_close_dicts") {
@@ -389,7 +385,7 @@
                         // The synchronously loaded contacts dictionary should have been in one
                         // or several pools, but it is shielded against multiple closing and it's
                         // safe to call it several times.
-                        final Dictionary dictToClose = mContactsDictionary;
+                        final ContactsBinaryDictionary dictToClose = mContactsDictionary;
                         // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY
                         // is no longer needed
                         mContactsDictionary = null;
@@ -421,13 +417,9 @@
                 DictionaryFactory.createMainDictionaryFromManager(this, locale,
                         true /* useFullEditDistance */);
         final String localeStr = locale.toString();
-        Dictionary userDictionary = mUserDictionaries.get(localeStr);
+        UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr);
         if (null == userDictionary) {
-            if (LatinIME.USE_BINARY_USER_DICTIONARY) {
-                userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
-            } else {
-                userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
-            }
+            userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
             mUserDictionaries.put(localeStr, userDictionary);
         }
         dictionaryCollection.addDictionary(userDictionary);
@@ -440,17 +432,11 @@
         synchronized (mUseContactsLock) {
             if (mUseContactsDictionary) {
                 if (null == mContactsDictionary) {
-                    // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
-                    // longer needed
-                    if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
-                        // TODO: use the right locale. We can't do it right now because the
-                        // spell checker is reusing the contacts dictionary across sessions
-                        // without regard for their locale, so we need to fix that first.
-                        mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
-                                Locale.getDefault());
-                    } else {
-                        mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
-                    }
+                    // TODO: use the right locale. We can't do it right now because the
+                    // spell checker is reusing the contacts dictionary across sessions
+                    // without regard for their locale, so we need to fix that first.
+                    mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
+                            Locale.getDefault());
                 }
             }
             dictionaryCollection.addDictionary(mContactsDictionary);
@@ -501,20 +487,32 @@
         }
 
         private static class SuggestionsCache {
+            private static final char CHAR_DELIMITER = '\uFFFC';
             private static final int MAX_CACHE_SIZE = 50;
-            // TODO: support bigram
             private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
                     new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
 
-            public SuggestionsParams getSuggestionsFromCache(String query) {
-                return mUnigramSuggestionsInfoCache.get(query);
+            // TODO: Support n-gram input
+            private static String generateKey(String query, String prevWord) {
+                if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) {
+                    return query;
+                }
+                return query + CHAR_DELIMITER + prevWord;
             }
 
-            public void putSuggestionsToCache(String query, String[] suggestions, int flags) {
+            // TODO: Support n-gram input
+            public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) {
+                return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord));
+            }
+
+            // TODO: Support n-gram input
+            public void putSuggestionsToCache(
+                    String query, String prevWord, String[] suggestions, int flags) {
                 if (suggestions == null || TextUtils.isEmpty(query)) {
                     return;
                 }
-                mUnigramSuggestionsInfoCache.put(query, new SuggestionsParams(suggestions, flags));
+                mUnigramSuggestionsInfoCache.put(
+                        generateKey(query, prevWord), new SuggestionsParams(suggestions, flags));
             }
         }
 
@@ -609,6 +607,7 @@
             final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
             final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
                     new ArrayList<SuggestionsInfo>();
+            String currentWord = null;
             for (int i = 0; i < N; ++i) {
                 final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
                 final int flags = si.getSuggestionsAttributes();
@@ -618,6 +617,8 @@
                 final int offset = ssi.getOffsetAt(i);
                 final int length = ssi.getLengthAt(i);
                 final String subText = typedText.substring(offset, offset + length);
+                final String prevWord = currentWord;
+                currentWord = subText;
                 if (!subText.contains(SINGLE_QUOTE)) {
                     continue;
                 }
@@ -631,7 +632,8 @@
                     if (TextUtils.isEmpty(splitText)) {
                         continue;
                     }
-                    if (mSuggestionsCache.getSuggestionsFromCache(splitText) == null) {
+                    if (mSuggestionsCache.getSuggestionsFromCache(
+                            splitText, prevWord) == null) {
                         continue;
                     }
                     final int newLength = splitText.length();
@@ -727,7 +729,7 @@
             try {
                 final String inText = textInfo.getText();
                 final SuggestionsParams cachedSuggestionsParams =
-                        mSuggestionsCache.getSuggestionsFromCache(inText);
+                        mSuggestionsCache.getSuggestionsFromCache(inText, prevWord);
                 if (cachedSuggestionsParams != null) {
                     if (DBG) {
                         Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
@@ -819,7 +821,7 @@
                                         .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
                                 : 0);
                 final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
-                mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags);
+                mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags);
                 return retval;
             } catch (RuntimeException e) {
                 // Don't kill the keyboard if there is a bug in the spell checker
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index d53757f..3bb7b58 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -18,7 +18,7 @@
 # If you change any of those flags, you need to rebuild both libjni_latinime_static
 # and the shared library.
 #FLAG_DBG := true
-#FLAG_DO_PROFILE := true
+FLAG_DO_PROFILE ?= false
 
 ######################################
 include $(CLEAR_VARS)
@@ -35,6 +35,7 @@
 LATIN_IME_JNI_SRC_FILES := \
     com_android_inputmethod_keyboard_ProximityInfo.cpp \
     com_android_inputmethod_latin_BinaryDictionary.cpp \
+    com_android_inputmethod_latin_NativeUtils.cpp \
     jni_common.cpp
 
 LATIN_IME_CORE_SRC_FILES := \
@@ -45,6 +46,7 @@
     correction.cpp \
     dictionary.cpp \
     proximity_info.cpp \
+    proximity_info_state.cpp \
     unigram_dictionary.cpp
 
 LOCAL_SRC_FILES := \
diff --git a/native/jni/com_android_inputmethod_latin_NativeUtils.cpp b/native/jni/com_android_inputmethod_latin_NativeUtils.cpp
new file mode 100644
index 0000000..c1e586a
--- /dev/null
+++ b/native/jni/com_android_inputmethod_latin_NativeUtils.cpp
@@ -0,0 +1,40 @@
+/*
+**
+** Copyright 2012, 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 "com_android_inputmethod_latin_NativeUtils.h"
+#include "jni.h"
+#include "jni_common.h"
+
+#include <math.h>
+
+namespace latinime {
+
+static float latinime_NativeUtils_powf(float x, float y) {
+    return powf(x, y);
+}
+
+static JNINativeMethod sMethods[] = {
+    {"powf", "(FF)F", (void*)latinime_NativeUtils_powf}
+};
+
+int register_NativeUtils(JNIEnv *env) {
+    const char* const kClassPathName = "com/android/inputmethod/latin/NativeUtils";
+    return registerNativeMethods(env, kClassPathName, sMethods,
+            sizeof(sMethods) / sizeof(sMethods[0]));
+}
+
+} // namespace latinime
diff --git a/native/jni/com_android_inputmethod_latin_NativeUtils.h b/native/jni/com_android_inputmethod_latin_NativeUtils.h
new file mode 100644
index 0000000..13a348a
--- /dev/null
+++ b/native/jni/com_android_inputmethod_latin_NativeUtils.h
@@ -0,0 +1,29 @@
+/*
+**
+** Copyright 2012, 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 _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
+#define _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
+
+#include "jni.h"
+
+namespace latinime {
+
+int register_NativeUtils(JNIEnv *env);
+
+}
+
+#endif // _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
diff --git a/native/jni/jni_common.cpp b/native/jni/jni_common.cpp
index b9e2c32..1314bab 100644
--- a/native/jni/jni_common.cpp
+++ b/native/jni/jni_common.cpp
@@ -19,6 +19,7 @@
 
 #include "com_android_inputmethod_keyboard_ProximityInfo.h"
 #include "com_android_inputmethod_latin_BinaryDictionary.h"
+#include "com_android_inputmethod_latin_NativeUtils.h"
 #include "defines.h"
 #include "jni.h"
 #include "proximity_info.h"
@@ -52,6 +53,11 @@
         goto bail;
     }
 
+    if (!register_NativeUtils(env)) {
+        AKLOGE("ERROR: NativeUtils native registration failed");
+        goto bail;
+    }
+
     /* success -- return valid version number */
     result = JNI_VERSION_1_4;
 
diff --git a/native/jni/src/additional_proximity_chars.h b/native/jni/src/additional_proximity_chars.h
index e0ecc0e..82c31f8 100644
--- a/native/jni/src/additional_proximity_chars.h
+++ b/native/jni/src/additional_proximity_chars.h
@@ -26,6 +26,7 @@
 
 class AdditionalProximityChars {
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(AdditionalProximityChars);
     static const std::string LOCALE_EN_US;
     static const int EN_US_ADDITIONAL_A_SIZE = 4;
     static const int32_t EN_US_ADDITIONAL_A[];
diff --git a/native/jni/src/bigram_dictionary.cpp b/native/jni/src/bigram_dictionary.cpp
index 9ef024d..1443369 100644
--- a/native/jni/src/bigram_dictionary.cpp
+++ b/native/jni/src/bigram_dictionary.cpp
@@ -27,9 +27,8 @@
 
 namespace latinime {
 
-BigramDictionary::BigramDictionary(const unsigned char *dict, int maxWordLength,
-        Dictionary *parentDictionary)
-    : DICT(dict), MAX_WORD_LENGTH(maxWordLength), mParentDictionary(parentDictionary) {
+BigramDictionary::BigramDictionary(const unsigned char *dict, int maxWordLength)
+        : DICT(dict), MAX_WORD_LENGTH(maxWordLength) {
     if (DEBUG_DICT) {
         AKLOGI("BigramDictionary - constructor");
     }
@@ -38,7 +37,8 @@
 BigramDictionary::~BigramDictionary() {
 }
 
-bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequency) {
+bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequency,
+        const int maxBigrams, int *bigramFreq, unsigned short *bigramChars) const {
     word[length] = 0;
     if (DEBUG_DICT) {
 #ifdef FLAG_DBG
@@ -50,25 +50,25 @@
 
     // Find the right insertion point
     int insertAt = 0;
-    while (insertAt < mMaxBigrams) {
-        if (frequency > mBigramFreq[insertAt] || (mBigramFreq[insertAt] == frequency
-                && length < Dictionary::wideStrLen(mBigramChars + insertAt * MAX_WORD_LENGTH))) {
+    while (insertAt < maxBigrams) {
+        if (frequency > bigramFreq[insertAt] || (bigramFreq[insertAt] == frequency
+                && length < Dictionary::wideStrLen(bigramChars + insertAt * MAX_WORD_LENGTH))) {
             break;
         }
         insertAt++;
     }
     if (DEBUG_DICT) {
-        AKLOGI("Bigram: InsertAt -> %d maxBigrams: %d", insertAt, mMaxBigrams);
+        AKLOGI("Bigram: InsertAt -> %d maxBigrams: %d", insertAt, maxBigrams);
     }
-    if (insertAt < mMaxBigrams) {
-        memmove((char*) mBigramFreq + (insertAt + 1) * sizeof(mBigramFreq[0]),
-               (char*) mBigramFreq + insertAt * sizeof(mBigramFreq[0]),
-               (mMaxBigrams - insertAt - 1) * sizeof(mBigramFreq[0]));
-        mBigramFreq[insertAt] = frequency;
-        memmove((char*) mBigramChars + (insertAt + 1) * MAX_WORD_LENGTH * sizeof(short),
-               (char*) mBigramChars + (insertAt    ) * MAX_WORD_LENGTH * sizeof(short),
-               (mMaxBigrams - insertAt - 1) * sizeof(short) * MAX_WORD_LENGTH);
-        unsigned short *dest = mBigramChars + (insertAt    ) * MAX_WORD_LENGTH;
+    if (insertAt < maxBigrams) {
+        memmove((char*) bigramFreq + (insertAt + 1) * sizeof(bigramFreq[0]),
+               (char*) bigramFreq + insertAt * sizeof(bigramFreq[0]),
+               (maxBigrams - insertAt - 1) * sizeof(bigramFreq[0]));
+        bigramFreq[insertAt] = frequency;
+        memmove((char*) bigramChars + (insertAt + 1) * MAX_WORD_LENGTH * sizeof(short),
+               (char*) bigramChars + (insertAt    ) * MAX_WORD_LENGTH * sizeof(short),
+               (maxBigrams - insertAt - 1) * sizeof(short) * MAX_WORD_LENGTH);
+        unsigned short *dest = bigramChars + (insertAt    ) * MAX_WORD_LENGTH;
         while (length--) {
             *dest++ = *word++;
         }
@@ -84,7 +84,7 @@
 /* Parameters :
  * prevWord: the word before, the one for which we need to look up bigrams.
  * prevWordLength: its length.
- * codes: what user typed, in the same format as for UnigramDictionary::getSuggestions.
+ * inputCodes: what user typed, in the same format as for UnigramDictionary::getSuggestions.
  * codesSize: the size of the codes array.
  * bigramChars: an array for output, at the same format as outwords for getSuggestions.
  * bigramFreq: an array to output frequencies.
@@ -98,15 +98,11 @@
  * and the bigrams are used to boost unigram result scores, it makes little sense to
  * reduce their scope to the ones that match the first letter.
  */
-int BigramDictionary::getBigrams(const int32_t *prevWord, int prevWordLength, int *codes,
+int BigramDictionary::getBigrams(const int32_t *prevWord, int prevWordLength, int *inputCodes,
         int codesSize, unsigned short *bigramChars, int *bigramFreq, int maxWordLength,
-        int maxBigrams) {
+        int maxBigrams) const {
     // TODO: remove unused arguments, and refrain from storing stuff in members of this class
     // TODO: have "in" arguments before "out" ones, and make out args explicit in the name
-    mBigramFreq = bigramFreq;
-    mBigramChars = bigramChars;
-    mInputCodes = codes;
-    mMaxBigrams = maxBigrams;
 
     const uint8_t* const root = DICT;
     int pos = getBigramListPositionForWord(prevWord, prevWordLength);
@@ -124,16 +120,17 @@
                 bigramBuffer, &unigramFreq);
 
         // codesSize == 0 means we are trying to find bigram predictions.
-        if (codesSize < 1 || checkFirstCharacter(bigramBuffer)) {
-            const int bigramFreq = UnigramDictionary::MASK_ATTRIBUTE_FREQUENCY & bigramFlags;
+        if (codesSize < 1 || checkFirstCharacter(bigramBuffer, inputCodes)) {
+            const int bigramFreqTemp = UnigramDictionary::MASK_ATTRIBUTE_FREQUENCY & bigramFlags;
             // Due to space constraints, the frequency for bigrams is approximate - the lower the
             // unigram frequency, the worse the precision. The theoritical maximum error in
             // resulting frequency is 8 - although in the practice it's never bigger than 3 or 4
             // in very bad cases. This means that sometimes, we'll see some bigrams interverted
             // here, but it can't get too bad.
             const int frequency =
-                    BinaryFormat::computeFrequencyForBigram(unigramFreq, bigramFreq);
-            if (addWordBigram(bigramBuffer, length, frequency)) {
+                    BinaryFormat::computeFrequencyForBigram(unigramFreq, bigramFreqTemp);
+            if (addWordBigram(
+                    bigramBuffer, length, frequency, maxBigrams, bigramFreq, bigramChars)) {
                 ++bigramCount;
             }
         }
@@ -144,7 +141,7 @@
 // Returns a pointer to the start of the bigram list.
 // If the word is not found or has no bigrams, this function returns 0.
 int BigramDictionary::getBigramListPositionForWord(const int32_t *prevWord,
-        const int prevWordLength) {
+        const int prevWordLength) const {
     if (0 >= prevWordLength) return 0;
     const uint8_t* const root = DICT;
     int pos = BinaryFormat::getTerminalPosition(root, prevWord, prevWordLength);
@@ -164,7 +161,7 @@
 }
 
 void BigramDictionary::fillBigramAddressToFrequencyMapAndFilter(const int32_t *prevWord,
-        const int prevWordLength, std::map<int, int> *map, uint8_t *filter) {
+        const int prevWordLength, std::map<int, int> *map, uint8_t *filter) const {
     memset(filter, 0, BIGRAM_FILTER_BYTE_SIZE);
     const uint8_t* const root = DICT;
     int pos = getBigramListPositionForWord(prevWord, prevWordLength);
@@ -181,11 +178,10 @@
     } while (0 != (UnigramDictionary::FLAG_ATTRIBUTE_HAS_NEXT & bigramFlags));
 }
 
-bool BigramDictionary::checkFirstCharacter(unsigned short *word) {
+bool BigramDictionary::checkFirstCharacter(unsigned short *word, int *inputCodes) const {
     // Checks whether this word starts with same character or neighboring characters of
     // what user typed.
 
-    int *inputCodes = mInputCodes;
     int maxAlt = MAX_ALTERNATIVES;
     const unsigned short firstBaseChar = toBaseLowerCase(*word);
     while (maxAlt > 0) {
@@ -199,7 +195,7 @@
 }
 
 bool BigramDictionary::isValidBigram(const int32_t *word1, int length1, const int32_t *word2,
-        int length2) {
+        int length2) const {
     const uint8_t* const root = DICT;
     int pos = getBigramListPositionForWord(word1, length1);
     // getBigramListPositionForWord returns 0 if this word isn't in the dictionary or has no bigrams
diff --git a/native/jni/src/bigram_dictionary.h b/native/jni/src/bigram_dictionary.h
index b8763a5..1ff1b2e 100644
--- a/native/jni/src/bigram_dictionary.h
+++ b/native/jni/src/bigram_dictionary.h
@@ -27,34 +27,29 @@
 class Dictionary;
 class BigramDictionary {
  public:
-    BigramDictionary(const unsigned char *dict, int maxWordLength, Dictionary *parentDictionary);
-    int getBigrams(const int32_t *word, int length, int *codes, int codesSize,
-            unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams);
-    int getBigramListPositionForWord(const int32_t *prevWord, const int prevWordLength);
+    BigramDictionary(const unsigned char *dict, int maxWordLength);
+    int getBigrams(const int32_t *word, int length, int *inputCodes, int codesSize,
+            unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams) const;
+    int getBigramListPositionForWord(const int32_t *prevWord, const int prevWordLength) const;
     void fillBigramAddressToFrequencyMapAndFilter(const int32_t *prevWord, const int prevWordLength,
-            std::map<int, int> *map, uint8_t *filter);
-    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2);
+            std::map<int, int> *map, uint8_t *filter) const;
+    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2) const;
     ~BigramDictionary();
  private:
-    bool addWordBigram(unsigned short *word, int length, int frequency);
+    DISALLOW_IMPLICIT_CONSTRUCTORS(BigramDictionary);
+    bool addWordBigram(unsigned short *word, int length, int frequency, const int maxBigrams,
+            int *bigramFreq, unsigned short *bigramChars) const;
     int getBigramAddress(int *pos, bool advance);
     int getBigramFreq(int *pos);
     void searchForTerminalNode(int addressLookingFor, int frequency);
     bool getFirstBitOfByte(int *pos) { return (DICT[*pos] & 0x80) > 0; }
     bool getSecondBitOfByte(int *pos) { return (DICT[*pos] & 0x40) > 0; }
-    bool checkFirstCharacter(unsigned short *word);
+    bool checkFirstCharacter(unsigned short *word, int *inputCodes) const;
 
     const unsigned char *DICT;
     const int MAX_WORD_LENGTH;
     // TODO: Re-implement proximity correction for bigram correction
     static const int MAX_ALTERNATIVES = 1;
-
-    Dictionary *mParentDictionary;
-    int *mBigramFreq;
-    int mMaxBigrams;
-    unsigned short *mBigramChars;
-    int *mInputCodes;
-    int mInputLength;
 };
 
 } // namespace latinime
diff --git a/native/jni/src/binary_format.h b/native/jni/src/binary_format.h
index 51bf8eb..214ecfa 100644
--- a/native/jni/src/binary_format.h
+++ b/native/jni/src/binary_format.h
@@ -25,6 +25,7 @@
 
 class BinaryFormat {
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(BinaryFormat);
     const static int32_t MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
     const static int32_t CHARACTER_ARRAY_TERMINATOR = 0x1F;
     const static int MULTIPLE_BYTE_CHARACTER_ADDITIONAL_SIZE = 2;
diff --git a/native/jni/src/correction.cpp b/native/jni/src/correction.cpp
index 99f5b92..827067b 100644
--- a/native/jni/src/correction.cpp
+++ b/native/jni/src/correction.cpp
@@ -27,6 +27,7 @@
 #include "defines.h"
 #include "dictionary.h"
 #include "proximity_info.h"
+#include "proximity_info_state.h"
 
 namespace latinime {
 
@@ -97,7 +98,7 @@
 static const char QUOTE = '\'';
 
 inline bool Correction::isQuote(const unsigned short c) {
-    const unsigned short userTypedChar = mProximityInfo->getPrimaryCharAt(mInputIndex);
+    const unsigned short userTypedChar = mProximityInfoState.getPrimaryCharAt(mInputIndex);
     return (c == QUOTE && userTypedChar != QUOTE);
 }
 
@@ -105,11 +106,6 @@
 // Correction //
 ////////////////
 
-Correction::Correction(const int typedLetterMultiplier, const int fullWordMultiplier)
-        : TYPED_LETTER_MULTIPLIER(typedLetterMultiplier), FULL_WORD_MULTIPLIER(fullWordMultiplier) {
-    initEditDistance(mEditDistanceTable);
-}
-
 void Correction::resetCorrection() {
     mTotalTraverseCount = 0;
 }
@@ -282,7 +278,7 @@
 
 void Correction::addCharToCurrentWord(const int32_t c) {
     mWord[mOutputIndex] = c;
-    const unsigned short *primaryInputWord = mProximityInfo->getPrimaryInputWord();
+    const unsigned short *primaryInputWord = mProximityInfoState.getPrimaryInputWord();
     calcEditDistanceOneStep(mEditDistanceTable, primaryInputWord, mInputLength,
             mWord, mOutputIndex + 1);
 }
@@ -308,13 +304,12 @@
     return UNRELATED;
 }
 
-inline bool isEquivalentChar(ProximityInfo::ProximityType type) {
-    return type == ProximityInfo::EQUIVALENT_CHAR;
+inline bool isEquivalentChar(ProximityType type) {
+    return type == EQUIVALENT_CHAR;
 }
 
-inline bool isProximityCharOrEquivalentChar(ProximityInfo::ProximityType type) {
-    return type == ProximityInfo::EQUIVALENT_CHAR
-            || type == ProximityInfo::NEAR_PROXIMITY_CHAR;
+inline bool isProximityCharOrEquivalentChar(ProximityType type) {
+    return type == EQUIVALENT_CHAR || type == NEAR_PROXIMITY_CHAR;
 }
 
 Correction::CorrectionType Correction::processCharAndCalcState(
@@ -335,19 +330,19 @@
         bool incremented = false;
         if (mLastCharExceeded && mInputIndex == mInputLength - 1) {
             // TODO: Do not check the proximity if EditDistance exceeds the threshold
-            const ProximityInfo::ProximityType matchId =
-                    mProximityInfo->getMatchedProximityId(mInputIndex, c, true, &proximityIndex);
+            const ProximityType matchId = mProximityInfoState.getMatchedProximityId(
+                    mInputIndex, c, true, &proximityIndex);
             if (isEquivalentChar(matchId)) {
                 mLastCharExceeded = false;
                 --mExcessiveCount;
                 mDistances[mOutputIndex] =
-                        mProximityInfo->getNormalizedSquaredDistance(mInputIndex, 0);
-            } else if (matchId == ProximityInfo::NEAR_PROXIMITY_CHAR) {
+                        mProximityInfoState.getNormalizedSquaredDistance(mInputIndex, 0);
+            } else if (matchId == NEAR_PROXIMITY_CHAR) {
                 mLastCharExceeded = false;
                 --mExcessiveCount;
                 ++mProximityCount;
-                mDistances[mOutputIndex] =
-                        mProximityInfo->getNormalizedSquaredDistance(mInputIndex, proximityIndex);
+                mDistances[mOutputIndex] = mProximityInfoState.getNormalizedSquaredDistance(
+                        mInputIndex, proximityIndex);
             }
             if (!isQuote(c)) {
                 incrementInputIndex();
@@ -388,7 +383,8 @@
 
     bool secondTransposing = false;
     if (mTransposedCount % 2 == 1) {
-        if (isEquivalentChar(mProximityInfo->getMatchedProximityId(mInputIndex - 1, c, false))) {
+        if (isEquivalentChar(mProximityInfoState.getMatchedProximityId(
+                mInputIndex - 1, c, false))) {
             ++mTransposedCount;
             secondTransposing = true;
         } else if (mCorrectionStates[mOutputIndex].mExceeding) {
@@ -417,17 +413,17 @@
             ? (noCorrectionsHappenedSoFar || mProximityCount == 0)
             : (noCorrectionsHappenedSoFar && mProximityCount == 0);
 
-    ProximityInfo::ProximityType matchedProximityCharId = secondTransposing
-            ? ProximityInfo::EQUIVALENT_CHAR
-            : mProximityInfo->getMatchedProximityId(
+    ProximityType matchedProximityCharId = secondTransposing
+            ? EQUIVALENT_CHAR
+            : mProximityInfoState.getMatchedProximityId(
                     mInputIndex, c, checkProximityChars, &proximityIndex);
 
-    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId
-            || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+    if (UNRELATED_CHAR == matchedProximityCharId
+            || ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
         if (canTryCorrection && mOutputIndex > 0
                 && mCorrectionStates[mOutputIndex].mProximityMatching
                 && mCorrectionStates[mOutputIndex].mExceeding
-                && isEquivalentChar(mProximityInfo->getMatchedProximityId(
+                && isEquivalentChar(mProximityInfoState.getMatchedProximityId(
                         mInputIndex, mWord[mOutputIndex - 1], false))) {
             if (DEBUG_CORRECTION
                     && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
@@ -446,14 +442,14 @@
             // Here, we are doing something equivalent to matchedProximityCharId,
             // but we already know that "excessive char correction" just happened
             // so that we just need to check "mProximityCount == 0".
-            matchedProximityCharId = mProximityInfo->getMatchedProximityId(
+            matchedProximityCharId = mProximityInfoState.getMatchedProximityId(
                     mInputIndex, c, mProximityCount == 0, &proximityIndex);
         }
     }
 
-    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId
-            || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
-        if (ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+    if (UNRELATED_CHAR == matchedProximityCharId
+            || ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+        if (ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
             mAdditionalProximityMatching = true;
         }
         // TODO: Optimize
@@ -463,10 +459,10 @@
         if (mInputIndex < mInputLength - 1 && mOutputIndex > 0 && mTransposedCount > 0
                 && !mCorrectionStates[mOutputIndex].mTransposing
                 && mCorrectionStates[mOutputIndex - 1].mTransposing
-                && isEquivalentChar(mProximityInfo->getMatchedProximityId(
+                && isEquivalentChar(mProximityInfoState.getMatchedProximityId(
                         mInputIndex, mWord[mOutputIndex - 1], false))
                 && isEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex + 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex + 1, c, false))) {
             // Conversion t->e
             // Example:
             // occaisional -> occa   sional
@@ -478,7 +474,7 @@
                 && !mCorrectionStates[mOutputIndex].mTransposing
                 && mCorrectionStates[mOutputIndex - 1].mTransposing
                 && isEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex - 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex - 1, c, false))) {
             // Conversion t->s
             // Example:
             // chcolate -> chocolate
@@ -490,7 +486,7 @@
                 && mCorrectionStates[mOutputIndex].mProximityMatching
                 && mCorrectionStates[mOutputIndex].mSkipping
                 && isEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex - 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex - 1, c, false))) {
             // Conversion p->s
             // Note: This logic tries saving cases like contrst --> contrast -- "a" is one of
             // proximity chars of "s", but it should rather be handled as a skipped char.
@@ -502,7 +498,7 @@
                 && mCorrectionStates[mOutputIndex].mSkipping
                 && mCorrectionStates[mOutputIndex].mAdditionalProximityMatching
                 && isProximityCharOrEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex + 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex + 1, c, false))) {
             // Conversion s->a
             incrementInputIndex();
             --mSkippedCount;
@@ -511,7 +507,7 @@
             mDistances[mOutputIndex] = ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO;
         } else if ((mExceeding || mTransposing) && mInputIndex - 1 < mInputLength
                 && isEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex + 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex + 1, c, false))) {
             // 1.2. Excessive or transpose correction
             if (mTransposing) {
                 ++mTransposedCount;
@@ -543,7 +539,7 @@
                         mTransposedCount, mExcessiveCount, c);
             }
             return processSkipChar(c, isTerminal, false);
-        } else if (ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+        } else if (ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
             // As a last resort, use additional proximity characters
             mProximityMatching = true;
             ++mProximityCount;
@@ -573,12 +569,12 @@
     } else if (isEquivalentChar(matchedProximityCharId)) {
         mMatching = true;
         ++mEquivalentCharCount;
-        mDistances[mOutputIndex] = mProximityInfo->getNormalizedSquaredDistance(mInputIndex, 0);
-    } else if (ProximityInfo::NEAR_PROXIMITY_CHAR == matchedProximityCharId) {
+        mDistances[mOutputIndex] = mProximityInfoState.getNormalizedSquaredDistance(mInputIndex, 0);
+    } else if (NEAR_PROXIMITY_CHAR == matchedProximityCharId) {
         mProximityMatching = true;
         ++mProximityCount;
         mDistances[mOutputIndex] =
-                mProximityInfo->getNormalizedSquaredDistance(mInputIndex, proximityIndex);
+                mProximityInfoState.getNormalizedSquaredDistance(mInputIndex, proximityIndex);
         if (DEBUG_CORRECTION
                 && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
                 && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
@@ -662,7 +658,7 @@
     const int excessivePos = correction->getExcessivePos();
     const int typedLetterMultiplier = correction->TYPED_LETTER_MULTIPLIER;
     const int fullWordMultiplier = correction->FULL_WORD_MULTIPLIER;
-    const ProximityInfo *proximityInfo = correction->mProximityInfo;
+    const ProximityInfoState *proximityInfoState = &correction->mProximityInfoState;
     const int skippedCount = correction->mSkippedCount;
     const int transposedCount = correction->mTransposedCount / 2;
     const int excessiveCount = correction->mExcessiveCount + correction->mTransposedCount % 2;
@@ -685,7 +681,7 @@
     const bool skipped = skippedCount > 0;
 
     const int quoteDiffCount = max(0, getQuoteCount(word, outputLength)
-            - getQuoteCount(proximityInfo->getPrimaryInputWord(), inputLength));
+            - getQuoteCount(proximityInfoState->getPrimaryInputWord(), inputLength));
 
     // TODO: Calculate edit distance for transposed and excessive
     int ed = 0;
@@ -737,8 +733,7 @@
         multiplyIntCapped(matchWeight, &finalFreq);
     }
 
-    if (proximityInfo->getMatchedProximityId(0, word[0], true)
-            == ProximityInfo::UNRELATED_CHAR) {
+    if (proximityInfoState->getMatchedProximityId(0, word[0], true) == UNRELATED_CHAR) {
         multiplyRate(FIRST_CHAR_DIFFERENT_DEMOTION_RATE, &finalFreq);
     }
 
@@ -764,7 +759,7 @@
     // Demotion for a word with excessive character
     if (excessiveCount > 0) {
         multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_DEMOTION_RATE, &finalFreq);
-        if (!lastCharExceeded && !proximityInfo->existsAdjacentProximityChars(excessivePos)) {
+        if (!lastCharExceeded && !proximityInfoState->existsAdjacentProximityChars(excessivePos)) {
             if (DEBUG_DICT_FULL) {
                 AKLOGI("Double excessive demotion");
             }
@@ -775,8 +770,9 @@
     }
 
     const bool performTouchPositionCorrection =
-            CALIBRATE_SCORE_BY_TOUCH_COORDINATES && proximityInfo->touchPositionCorrectionEnabled()
-                        && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0;
+            CALIBRATE_SCORE_BY_TOUCH_COORDINATES
+                    && proximityInfoState->touchPositionCorrectionEnabled()
+                    && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0;
     // Score calibration by touch coordinates is being done only for pure-fat finger typing error
     // cases.
     int additionalProximityCount = 0;
@@ -796,7 +792,7 @@
                 static const float R1 = NEUTRAL_SCORE_SQUARED_RADIUS;
                 static const float R2 = HALF_SCORE_SQUARED_RADIUS;
                 const float x = (float)squaredDistance
-                        / ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
+                        / ProximityInfoState::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
                 const float factor = max((x < R1)
                     ? (A * (R1 - x) + B * x) / R1
                     : (B * (R2 - x) + C * (x - R1)) / (R2 - R1), MIN);
@@ -907,7 +903,7 @@
 
     if (DEBUG_CORRECTION_FREQ
             && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputLength)) {
-        DUMP_WORD(proximityInfo->getPrimaryInputWord(), inputLength);
+        DUMP_WORD(correction->getPrimaryInputWord(), inputLength);
         DUMP_WORD(correction->mWord, outputLength);
         AKLOGI("FinalFreq: [P%d, S%d, T%d, E%d, A%d] %d, %d, %d, %d, %d, %d", proximityMatchedCount,
                 skippedCount, transposedCount, excessiveCount, additionalProximityCount,
@@ -1146,5 +1142,4 @@
     const float weight = 1.0 - (float) distance / afterLength;
     return (score / maxScore) * weight;
 }
-
 } // namespace latinime
diff --git a/native/jni/src/correction.h b/native/jni/src/correction.h
index 3300a84..ae7b3a5 100644
--- a/native/jni/src/correction.h
+++ b/native/jni/src/correction.h
@@ -19,9 +19,10 @@
 
 #include <assert.h>
 #include <stdint.h>
-#include "correction_state.h"
 
+#include "correction_state.h"
 #include "defines.h"
+#include "proximity_info_state.h"
 
 namespace latinime {
 
@@ -93,7 +94,7 @@
         }
     }
 
-    Correction(const int typedLetterMultiplier, const int fullWordMultiplier);
+    Correction() {};
     void resetCorrection();
     void initCorrection(
             const ProximityInfo *pi, const int inputLength, const int maxWordLength);
@@ -174,11 +175,25 @@
      private:
         static const int CODE_SPACE = ' ';
         static const int MAX_INITIAL_SCORE = 255;
-        static const int TYPED_LETTER_MULTIPLIER = 2;
-        static const int FULL_WORD_MULTIPLIER = 2;
     };
 
+    // proximity info state
+    void initInputParams(const ProximityInfo *proximityInfo, const int32_t *inputCodes,
+            const int inputLength, const int *xCoordinates, const int *yCoordinates) {
+        mProximityInfoState.initInputParams(
+                proximityInfo, inputCodes, inputLength, xCoordinates, yCoordinates);
+    }
+
+    const unsigned short* getPrimaryInputWord() const {
+        return mProximityInfoState.getPrimaryInputWord();
+    }
+
+    unsigned short getPrimaryCharAt(const int index) const {
+        return mProximityInfoState.getPrimaryCharAt(index);
+    }
+
  private:
+    DISALLOW_COPY_AND_ASSIGN(Correction);
     inline void incrementInputIndex();
     inline void incrementOutputIndex();
     inline void startToTraverseAllNodes();
@@ -190,8 +205,8 @@
     inline int getFinalProbabilityInternal(const int probability, unsigned short **word,
             int* wordLength, const int inputLength);
 
-    const int TYPED_LETTER_MULTIPLIER;
-    const int FULL_WORD_MULTIPLIER;
+    static const int TYPED_LETTER_MULTIPLIER = 2;
+    static const int FULL_WORD_MULTIPLIER = 2;
     const ProximityInfo *mProximityInfo;
 
     bool mUseFullEditDistance;
@@ -240,7 +255,7 @@
     bool mExceeding;
     bool mTransposing;
     bool mSkipping;
-
+    ProximityInfoState mProximityInfoState;
 };
 } // namespace latinime
 #endif // LATINIME_CORRECTION_H
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index cd2fc63..8bcadcb 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -225,6 +225,9 @@
 // This is only used for the size of array. Not to be used in c functions.
 #define MAX_WORD_LENGTH_INTERNAL 48
 
+// This must be the same as ProximityInfo#MAX_PROXIMITY_CHARS_SIZE, currently it's 16.
+#define MAX_PROXIMITY_CHARS_SIZE_INTERNAL 16
+
 // This must be equal to ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE in KeyDetector.java
 #define ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE 2
 
@@ -289,4 +292,24 @@
 #define INPUTLENGTH_FOR_DEBUG -1
 #define MIN_OUTPUT_INDEX_FOR_DEBUG -1
 
+#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
+  TypeName(const TypeName&);               \
+  void operator=(const TypeName&)
+
+#define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \
+  TypeName();                                    \
+  DISALLOW_COPY_AND_ASSIGN(TypeName)
+
+// Used as a return value for character comparison
+typedef enum {
+    // Same char, possibly with different case or accent
+    EQUIVALENT_CHAR,
+    // It is a char located nearby on the keyboard
+    NEAR_PROXIMITY_CHAR,
+    // It is an unrelated char
+    UNRELATED_CHAR,
+    // Additional proximity char which can differ by language.
+    ADDITIONAL_PROXIMITY_CHAR
+} ProximityType;
+
 #endif // LATINIME_DEFINES_H
diff --git a/native/jni/src/dictionary.cpp b/native/jni/src/dictionary.cpp
index 1fb0247..83bb267 100644
--- a/native/jni/src/dictionary.cpp
+++ b/native/jni/src/dictionary.cpp
@@ -38,29 +38,24 @@
             AKLOGI("IN NATIVE SUGGEST Version: %d", (mDict[0] & 0xFF));
         }
     }
-    mCorrection = new Correction(typedLetterMultiplier, fullWordMultiplier);
-    mWordsPriorityQueuePool = new WordsPriorityQueuePool(
-            maxWords, SUB_QUEUE_MAX_WORDS, maxWordLength);
     const unsigned int headerSize = BinaryFormat::getHeaderSize(mDict);
     const unsigned int options = BinaryFormat::getFlags(mDict);
     mUnigramDictionary = new UnigramDictionary(mDict + headerSize, typedLetterMultiplier,
             fullWordMultiplier, maxWordLength, maxWords, options);
-    mBigramDictionary = new BigramDictionary(mDict + headerSize, maxWordLength, this);
+    mBigramDictionary = new BigramDictionary(mDict + headerSize, maxWordLength);
 }
 
 Dictionary::~Dictionary() {
-    delete mCorrection;
-    delete mWordsPriorityQueuePool;
     delete mUnigramDictionary;
     delete mBigramDictionary;
 }
 
-int Dictionary::getFrequency(const int32_t *word, int length) {
+int Dictionary::getFrequency(const int32_t *word, int length) const {
     return mUnigramDictionary->getFrequency(word, length);
 }
 
 bool Dictionary::isValidBigram(const int32_t *word1, int length1, const int32_t *word2,
-        int length2) {
+        int length2) const {
     return mBigramDictionary->isValidBigram(word1, length1, word2, length2);
 }
 
diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h
index 9f23679..fd69f79 100644
--- a/native/jni/src/dictionary.h
+++ b/native/jni/src/dictionary.h
@@ -21,7 +21,6 @@
 
 #include "bigram_dictionary.h"
 #include "char_utils.h"
-#include "correction.h"
 #include "defines.h"
 #include "proximity_info.h"
 #include "unigram_dictionary.h"
@@ -36,28 +35,28 @@
 
     int getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates, int *ycoordinates,
             int *codes, int codesSize, const int32_t* prevWordChars, const int prevWordLength,
-            bool useFullEditDistance, unsigned short *outWords, int *frequencies) {
+            bool useFullEditDistance, unsigned short *outWords, int *frequencies) const {
         std::map<int, int> bigramMap;
         uint8_t bigramFilter[BIGRAM_FILTER_BYTE_SIZE];
         mBigramDictionary->fillBigramAddressToFrequencyMapAndFilter(prevWordChars,
                 prevWordLength, &bigramMap, bigramFilter);
-        return mUnigramDictionary->getSuggestions(proximityInfo, mWordsPriorityQueuePool,
-                mCorrection, xcoordinates, ycoordinates, codes, codesSize, &bigramMap,
+        return mUnigramDictionary->getSuggestions(proximityInfo,
+                xcoordinates, ycoordinates, codes, codesSize, &bigramMap,
                 bigramFilter, useFullEditDistance, outWords, frequencies);
     }
 
     int getBigrams(const int32_t *word, int length, int *codes, int codesSize,
-            unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams) {
+            unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams) const {
         return mBigramDictionary->getBigrams(word, length, codes, codesSize, outWords, frequencies,
                 maxWordLength, maxBigrams);
     }
 
-    int getFrequency(const int32_t *word, int length);
-    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2);
-    void *getDict() { return (void *)mDict; }
-    int getDictSize() { return mDictSize; }
-    int getMmapFd() { return mMmapFd; }
-    int getDictBufAdjust() { return mDictBufAdjust; }
+    int getFrequency(const int32_t *word, int length) const;
+    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2) const;
+    void *getDict() const { return (void *)mDict; }
+    int getDictSize() const { return mDictSize; }
+    int getMmapFd() const { return mMmapFd; }
+    int getDictBufAdjust() const { return mDictBufAdjust; }
     ~Dictionary();
 
     // public static utility methods
@@ -65,6 +64,7 @@
     static int wideStrLen(unsigned short *str);
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(Dictionary);
     const unsigned char *mDict;
 
     // Used only for the mmap version of dictionary loading, but we use these as dummy variables
@@ -73,10 +73,8 @@
     const int mMmapFd;
     const int mDictBufAdjust;
 
-    UnigramDictionary *mUnigramDictionary;
-    BigramDictionary *mBigramDictionary;
-    WordsPriorityQueuePool *mWordsPriorityQueuePool;
-    Correction *mCorrection;
+    const UnigramDictionary *mUnigramDictionary;
+    const BigramDictionary *mBigramDictionary;
 };
 
 // public static utility methods
diff --git a/native/jni/src/proximity_info.cpp b/native/jni/src/proximity_info.cpp
index 960d401..2ba244a 100644
--- a/native/jni/src/proximity_info.cpp
+++ b/native/jni/src/proximity_info.cpp
@@ -24,6 +24,7 @@
 #include "defines.h"
 #include "dictionary.h"
 #include "proximity_info.h"
+#include "proximity_info_state.h"
 
 namespace latinime {
 
@@ -51,23 +52,14 @@
           HAS_TOUCH_POSITION_CORRECTION_DATA(keyCount > 0 && keyXCoordinates && keyYCoordinates
                   && keyWidths && keyHeights && keyCharCodes && sweetSpotCenterXs
                   && sweetSpotCenterYs && sweetSpotRadii),
-          mLocaleStr(localeStr),
-          mInputXCoordinates(0), mInputYCoordinates(0),
-          mTouchPositionCorrectionEnabled(false) {
+          mLocaleStr(localeStr) {
     const int proximityGridLength = GRID_WIDTH * GRID_HEIGHT * MAX_PROXIMITY_CHARS_SIZE;
-    mProximityCharsArray = new int32_t[proximityGridLength];
-    mInputCodes = new int32_t[MAX_PROXIMITY_CHARS_SIZE * MAX_WORD_LENGTH_INTERNAL];
     if (DEBUG_PROXIMITY_INFO) {
         AKLOGI("Create proximity info array %d", proximityGridLength);
     }
+    mProximityCharsArray = new int32_t[proximityGridLength];
     memcpy(mProximityCharsArray, proximityCharsArray,
             proximityGridLength * sizeof(mProximityCharsArray[0]));
-    const int normalizedSquaredDistancesLength =
-            MAX_PROXIMITY_CHARS_SIZE * MAX_WORD_LENGTH_INTERNAL;
-    mNormalizedSquaredDistances = new int[normalizedSquaredDistancesLength];
-    for (int i = 0; i < normalizedSquaredDistancesLength; ++i) {
-        mNormalizedSquaredDistances[i] = NOT_A_DISTANCE;
-    }
 
     copyOrFillZero(mKeyXCoordinates, keyXCoordinates, KEY_COUNT * sizeof(mKeyXCoordinates[0]));
     copyOrFillZero(mKeyYCoordinates, keyYCoordinates, KEY_COUNT * sizeof(mKeyYCoordinates[0]));
@@ -96,9 +88,7 @@
 }
 
 ProximityInfo::~ProximityInfo() {
-    delete[] mNormalizedSquaredDistances;
     delete[] mProximityCharsArray;
-    delete[] mInputCodes;
 }
 
 inline int ProximityInfo::getStartIndexFromCoordinates(const int x, const int y) const {
@@ -119,26 +109,18 @@
     if (DEBUG_PROXIMITY_INFO) {
         AKLOGI("hasSpaceProximity: index %d, %d, %d", startIndex, x, y);
     }
+    int32_t* proximityCharsArray = mProximityCharsArray;
     for (int i = 0; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
         if (DEBUG_PROXIMITY_INFO) {
             AKLOGI("Index: %d", mProximityCharsArray[startIndex + i]);
         }
-        if (mProximityCharsArray[startIndex + i] == KEYCODE_SPACE) {
+        if (proximityCharsArray[startIndex + i] == KEYCODE_SPACE) {
             return true;
         }
     }
     return false;
 }
 
-bool ProximityInfo::isOnKey(const int keyId, const int x, const int y) const {
-    if (keyId < 0) return true; // NOT_A_ID is -1, but return whenever < 0 just in case
-    const int left = mKeyXCoordinates[keyId];
-    const int top = mKeyYCoordinates[keyId];
-    const int right = left + mKeyWidths[keyId] + 1;
-    const int bottom = top + mKeyHeights[keyId];
-    return left < right && top < bottom && x >= left && x < right && y >= top && y < bottom;
-}
-
 int ProximityInfo::squaredDistanceToEdge(const int keyId, const int x, const int y) const {
     if (keyId < 0) return true; // NOT_A_ID is -1, but return whenever < 0 just in case
     const int left = mKeyXCoordinates[keyId];
@@ -154,12 +136,13 @@
 
 void ProximityInfo::calculateNearbyKeyCodes(
         const int x, const int y, const int32_t primaryKey, int *inputCodes) const {
+    int32_t *proximityCharsArray = mProximityCharsArray;
     int insertPos = 0;
     inputCodes[insertPos++] = primaryKey;
     const int startIndex = getStartIndexFromCoordinates(x, y);
     if (startIndex >= 0) {
         for (int i = 0; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
-            const int32_t c = mProximityCharsArray[startIndex + i];
+            const int32_t c = proximityCharsArray[startIndex + i];
             if (c < KEYCODE_SPACE || c == primaryKey) {
                 continue;
             }
@@ -216,115 +199,6 @@
     }
 }
 
-void ProximityInfo::setInputParams(const int32_t* inputCodes, const int inputLength,
-        const int* xCoordinates, const int* yCoordinates) {
-    memset(mInputCodes, 0,
-            MAX_WORD_LENGTH_INTERNAL * MAX_PROXIMITY_CHARS_SIZE * sizeof(mInputCodes[0]));
-
-    for (int i = 0; i < inputLength; ++i) {
-        const int32_t primaryKey = inputCodes[i];
-        const int x = xCoordinates[i];
-        const int y = yCoordinates[i];
-        int *proximities = &mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE];
-        calculateNearbyKeyCodes(x, y, primaryKey, proximities);
-    }
-
-    if (DEBUG_PROXIMITY_CHARS) {
-        for (int i = 0; i < inputLength; ++i) {
-            AKLOGI("---");
-            for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE; ++j) {
-                int icc = mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE + j];
-                int icfjc = inputCodes[i * MAX_PROXIMITY_CHARS_SIZE + j];
-                icc+= 0;
-                icfjc += 0;
-                AKLOGI("--- (%d)%c,%c", i, icc, icfjc);
-                AKLOGI("---             A<%d>,B<%d>", icc, icfjc);
-            }
-        }
-    }
-    //Keep for debug, sorry
-    //for (int i = 0; i < MAX_WORD_LENGTH_INTERNAL * MAX_PROXIMITY_CHARS_SIZE; ++i) {
-    //if (i < inputLength * MAX_PROXIMITY_CHARS_SIZE) {
-    //mInputCodes[i] = mInputCodesFromJava[i];
-    //} else {
-    // mInputCodes[i] = 0;
-    // }
-    //}
-    mInputXCoordinates = xCoordinates;
-    mInputYCoordinates = yCoordinates;
-    mTouchPositionCorrectionEnabled =
-            HAS_TOUCH_POSITION_CORRECTION_DATA && xCoordinates && yCoordinates;
-    mInputLength = inputLength;
-    for (int i = 0; i < inputLength; ++i) {
-        mPrimaryInputWord[i] = getPrimaryCharAt(i);
-    }
-    mPrimaryInputWord[inputLength] = 0;
-    if (DEBUG_PROXIMITY_CHARS) {
-        AKLOGI("--- setInputParams");
-    }
-    for (int i = 0; i < mInputLength; ++i) {
-        const int *proximityChars = getProximityCharsAt(i);
-        const int primaryKey = proximityChars[0];
-        const int x = xCoordinates[i];
-        const int y = yCoordinates[i];
-        if (DEBUG_PROXIMITY_CHARS) {
-            int a = x + y + primaryKey;
-            a += 0;
-            AKLOGI("--- Primary = %c, x = %d, y = %d", primaryKey, x, y);
-            // Keep debug code just in case
-            //int proximities[50];
-            //for (int m = 0; m < 50; ++m) {
-            //proximities[m] = 0;
-            //}
-            //calculateNearbyKeyCodes(x, y, primaryKey, proximities);
-            //for (int l = 0; l < 50 && proximities[l] > 0; ++l) {
-            //if (DEBUG_PROXIMITY_CHARS) {
-            //AKLOGI("--- native Proximity (%d) = %c", l, proximities[l]);
-            //}
-            //}
-        }
-        for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE && proximityChars[j] > 0; ++j) {
-            const int currentChar = proximityChars[j];
-            const float squaredDistance = hasInputCoordinates()
-                    ? calculateNormalizedSquaredDistance(getKeyIndex(currentChar), i)
-                    : NOT_A_DISTANCE_FLOAT;
-            if (squaredDistance >= 0.0f) {
-                mNormalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE + j] =
-                        (int)(squaredDistance * NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR);
-            } else {
-                mNormalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE + j] = (j == 0)
-                        ? EQUIVALENT_CHAR_WITHOUT_DISTANCE_INFO
-                        : PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO;
-            }
-            if (DEBUG_PROXIMITY_CHARS) {
-                AKLOGI("--- Proximity (%d) = %c", j, currentChar);
-            }
-        }
-    }
-}
-
-inline float square(const float x) { return x * x; }
-
-float ProximityInfo::calculateNormalizedSquaredDistance(
-        const int keyIndex, const int inputIndex) const {
-    if (keyIndex == NOT_AN_INDEX) {
-        return NOT_A_DISTANCE_FLOAT;
-    }
-    if (!hasSweetSpotData(keyIndex)) {
-        return NOT_A_DISTANCE_FLOAT;
-    }
-    if (NOT_A_COORDINATE == mInputXCoordinates[inputIndex]) {
-        return NOT_A_DISTANCE_FLOAT;
-    }
-    const float squaredDistance = calculateSquaredDistanceFromSweetSpotCenter(keyIndex, inputIndex);
-    const float squaredRadius = square(mSweetSpotRadii[keyIndex]);
-    return squaredDistance / squaredRadius;
-}
-
-bool ProximityInfo::hasInputCoordinates() const {
-    return mInputXCoordinates && mInputYCoordinates;
-}
-
 int ProximityInfo::getKeyIndex(const int c) const {
     if (KEY_COUNT == 0) {
         // We do not have the coordinate data
@@ -336,132 +210,4 @@
     }
     return mCodeToKeyIndex[baseLowerC];
 }
-
-float ProximityInfo::calculateSquaredDistanceFromSweetSpotCenter(
-        const int keyIndex, const int inputIndex) const {
-    const float sweetSpotCenterX = mSweetSpotCenterXs[keyIndex];
-    const float sweetSpotCenterY = mSweetSpotCenterYs[keyIndex];
-    const float inputX = (float)mInputXCoordinates[inputIndex];
-    const float inputY = (float)mInputYCoordinates[inputIndex];
-    return square(inputX - sweetSpotCenterX) + square(inputY - sweetSpotCenterY);
-}
-
-inline const int* ProximityInfo::getProximityCharsAt(const int index) const {
-    return mInputCodes + (index * MAX_PROXIMITY_CHARS_SIZE);
-}
-
-unsigned short ProximityInfo::getPrimaryCharAt(const int index) const {
-    return getProximityCharsAt(index)[0];
-}
-
-inline bool ProximityInfo::existsCharInProximityAt(const int index, const int c) const {
-    const int *chars = getProximityCharsAt(index);
-    int i = 0;
-    while (chars[i] > 0 && i < MAX_PROXIMITY_CHARS_SIZE) {
-        if (chars[i++] == c) {
-            return true;
-        }
-    }
-    return false;
-}
-
-bool ProximityInfo::existsAdjacentProximityChars(const int index) const {
-    if (index < 0 || index >= mInputLength) return false;
-    const int currentChar = getPrimaryCharAt(index);
-    const int leftIndex = index - 1;
-    if (leftIndex >= 0 && existsCharInProximityAt(leftIndex, currentChar)) {
-        return true;
-    }
-    const int rightIndex = index + 1;
-    if (rightIndex < mInputLength && existsCharInProximityAt(rightIndex, currentChar)) {
-        return true;
-    }
-    return false;
-}
-
-// In the following function, c is the current character of the dictionary word
-// currently examined.
-// currentChars is an array containing the keys close to the character the
-// user actually typed at the same position. We want to see if c is in it: if so,
-// then the word contains at that position a character close to what the user
-// typed.
-// What the user typed is actually the first character of the array.
-// proximityIndex is a pointer to the variable where getMatchedProximityId returns
-// the index of c in the proximity chars of the input index.
-// Notice : accented characters do not have a proximity list, so they are alone
-// in their list. The non-accented version of the character should be considered
-// "close", but not the other keys close to the non-accented version.
-ProximityInfo::ProximityType ProximityInfo::getMatchedProximityId(const int index,
-        const unsigned short c, const bool checkProximityChars, int *proximityIndex) const {
-    const int *currentChars = getProximityCharsAt(index);
-    const int firstChar = currentChars[0];
-    const unsigned short baseLowerC = toBaseLowerCase(c);
-
-    // The first char in the array is what user typed. If it matches right away,
-    // that means the user typed that same char for this pos.
-    if (firstChar == baseLowerC || firstChar == c) {
-        return EQUIVALENT_CHAR;
-    }
-
-    if (!checkProximityChars) return UNRELATED_CHAR;
-
-    // If the non-accented, lowercased version of that first character matches c,
-    // then we have a non-accented version of the accented character the user
-    // typed. Treat it as a close char.
-    if (toBaseLowerCase(firstChar) == baseLowerC)
-        return NEAR_PROXIMITY_CHAR;
-
-    // Not an exact nor an accent-alike match: search the list of close keys
-    int j = 1;
-    while (j < MAX_PROXIMITY_CHARS_SIZE
-            && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
-        const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
-        if (matched) {
-            if (proximityIndex) {
-                *proximityIndex = j;
-            }
-            return NEAR_PROXIMITY_CHAR;
-        }
-        ++j;
-    }
-    if (j < MAX_PROXIMITY_CHARS_SIZE
-            && currentChars[j] == ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
-        ++j;
-        while (j < MAX_PROXIMITY_CHARS_SIZE
-                && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
-            const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
-            if (matched) {
-                if (proximityIndex) {
-                    *proximityIndex = j;
-                }
-                return ADDITIONAL_PROXIMITY_CHAR;
-            }
-            ++j;
-        }
-    }
-
-    // Was not included, signal this as an unrelated character.
-    return UNRELATED_CHAR;
-}
-
-bool ProximityInfo::sameAsTyped(const unsigned short *word, int length) const {
-    if (length != mInputLength) {
-        return false;
-    }
-    const int *inputCodes = mInputCodes;
-    while (length--) {
-        if ((unsigned int) *inputCodes != (unsigned int) *word) {
-            return false;
-        }
-        inputCodes += MAX_PROXIMITY_CHARS_SIZE;
-        word++;
-    }
-    return true;
-}
-
-const int ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2;
-const int ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
-const int ProximityInfo::MAX_KEY_COUNT_IN_A_KEYBOARD;
-const int ProximityInfo::MAX_CHAR_CODE;
-
 } // namespace latinime
diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h
index feb0c94..fec6555 100644
--- a/native/jni/src/proximity_info.h
+++ b/native/jni/src/proximity_info.h
@@ -28,22 +28,6 @@
 
 class ProximityInfo {
  public:
-    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2 = 10;
-    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR =
-            1 << NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2;
-
-    // Used as a return value for character comparison
-    typedef enum {
-        // Same char, possibly with different case or accent
-        EQUIVALENT_CHAR,
-        // It is a char located nearby on the keyboard
-        NEAR_PROXIMITY_CHAR,
-        // It is an unrelated char
-        UNRELATED_CHAR,
-        // Additional proximity char which can differ by language.
-        ADDITIONAL_PROXIMITY_CHAR
-    } ProximityType;
-
     ProximityInfo(const std::string localeStr, const int maxProximityCharsSize,
             const int keyboardWidth, const int keyboardHeight, const int gridWidth,
             const int gridHeight, const int mostCommonkeyWidth,
@@ -53,26 +37,69 @@
             const float *sweetSpotCenterYs, const float *sweetSpotRadii);
     ~ProximityInfo();
     bool hasSpaceProximity(const int x, const int y) const;
-    void setInputParams(const int32_t *inputCodes, const int inputLength,
-            const int *xCoordinates, const int *yCoordinates);
-    const int* getProximityCharsAt(const int index) const;
-    unsigned short getPrimaryCharAt(const int index) const;
-    bool existsCharInProximityAt(const int index, const int c) const;
-    bool existsAdjacentProximityChars(const int index) const;
-    ProximityType getMatchedProximityId(const int index, const unsigned short c,
-            const bool checkProximityChars, int *proximityIndex = 0) const;
-    int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const {
-        return mNormalizedSquaredDistances[inputIndex * MAX_PROXIMITY_CHARS_SIZE + proximityIndex];
-    }
+    int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const;
     bool sameAsTyped(const unsigned short *word, int length) const;
-    const unsigned short* getPrimaryInputWord() const {
-        return mPrimaryInputWord;
+    int squaredDistanceToEdge(const int keyId, const int x, const int y) const;
+    bool isOnKey(const int keyId, const int x, const int y) const {
+        if (keyId < 0) return true; // NOT_A_ID is -1, but return whenever < 0 just in case
+        const int left = mKeyXCoordinates[keyId];
+        const int top = mKeyYCoordinates[keyId];
+        const int right = left + mKeyWidths[keyId] + 1;
+        const int bottom = top + mKeyHeights[keyId];
+        return left < right && top < bottom && x >= left && x < right && y >= top && y < bottom;
     }
-    bool touchPositionCorrectionEnabled() const {
-        return mTouchPositionCorrectionEnabled;
+    int getKeyIndex(const int c) const;
+    bool hasSweetSpotData(const int keyIndex) const {
+        // When there are no calibration data for a key,
+        // the radius of the key is assigned to zero.
+        return mSweetSpotRadii[keyIndex] > 0.0;
+    }
+    float getSweetSpotRadiiAt(int keyIndex) const {
+        return mSweetSpotRadii[keyIndex];
+    }
+    float getSweetSpotCenterXAt(int keyIndex) const {
+        return mSweetSpotCenterXs[keyIndex];
+    }
+    float getSweetSpotCenterYAt(int keyIndex) const {
+        return mSweetSpotCenterYs[keyIndex];
+    }
+    void calculateNearbyKeyCodes(
+            const int x, const int y, const int32_t primaryKey, int *inputCodes) const;
+
+    bool hasTouchPositionCorrectionData() const {
+        return HAS_TOUCH_POSITION_CORRECTION_DATA;
+    }
+
+    int getMostCommonKeyWidthSquare() const {
+        return MOST_COMMON_KEY_WIDTH_SQUARE;
+    }
+
+    std::string getLocaleStr() const {
+        return mLocaleStr;
+    }
+
+    int getKeyCount() const {
+        return KEY_COUNT;
+    }
+
+    int getCellHeight() const {
+        return CELL_HEIGHT;
+    }
+
+    int getCellWidth() const {
+        return CELL_WIDTH;
+    }
+
+    int getGridWidth() const {
+        return GRID_WIDTH;
+    }
+
+    int getGridHeight() const {
+        return GRID_HEIGHT;
     }
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(ProximityInfo);
     // The max number of the keys in one keyboard layout
     static const int MAX_KEY_COUNT_IN_A_KEYBOARD = 64;
     // The upper limit of the char code in mCodeToKeyIndex
@@ -86,16 +113,6 @@
     float calculateSquaredDistanceFromSweetSpotCenter(
             const int keyIndex, const int inputIndex) const;
     bool hasInputCoordinates() const;
-    int getKeyIndex(const int c) const;
-    bool hasSweetSpotData(const int keyIndex) const {
-        // When there are no calibration data for a key,
-        // the radius of the key is assigned to zero.
-        return mSweetSpotRadii[keyIndex] > 0.0;
-    }
-    bool isOnKey(const int keyId, const int x, const int y) const;
-    int squaredDistanceToEdge(const int keyId, const int x, const int y) const;
-    void calculateNearbyKeyCodes(
-            const int x, const int y, const int32_t primaryKey, int *inputCodes) const;
 
     const int MAX_PROXIMITY_CHARS_SIZE;
     const int KEYBOARD_WIDTH;
@@ -108,14 +125,7 @@
     const int KEY_COUNT;
     const bool HAS_TOUCH_POSITION_CORRECTION_DATA;
     const std::string mLocaleStr;
-    // TODO: remove this
-    const int *mInputCodesFromJava;
-    int32_t *mInputCodes;
-    const int *mInputXCoordinates;
-    const int *mInputYCoordinates;
-    bool mTouchPositionCorrectionEnabled;
     int32_t *mProximityCharsArray;
-    int *mNormalizedSquaredDistances;
     int32_t mKeyXCoordinates[MAX_KEY_COUNT_IN_A_KEYBOARD];
     int32_t mKeyYCoordinates[MAX_KEY_COUNT_IN_A_KEYBOARD];
     int32_t mKeyWidths[MAX_KEY_COUNT_IN_A_KEYBOARD];
@@ -124,9 +134,8 @@
     float mSweetSpotCenterXs[MAX_KEY_COUNT_IN_A_KEYBOARD];
     float mSweetSpotCenterYs[MAX_KEY_COUNT_IN_A_KEYBOARD];
     float mSweetSpotRadii[MAX_KEY_COUNT_IN_A_KEYBOARD];
-    int mInputLength;
-    unsigned short mPrimaryInputWord[MAX_WORD_LENGTH_INTERNAL];
     int mCodeToKeyIndex[MAX_CHAR_CODE + 1];
+    // TODO: move to correction.h
 };
 
 } // namespace latinime
diff --git a/native/jni/src/proximity_info_state.cpp b/native/jni/src/proximity_info_state.cpp
new file mode 100644
index 0000000..149299e
--- /dev/null
+++ b/native/jni/src/proximity_info_state.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2012 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 <assert.h>
+#include <stdint.h>
+#include <string>
+
+#define LOG_TAG "LatinIME: proximity_info_state.cpp"
+
+#include "additional_proximity_chars.h"
+#include "defines.h"
+#include "dictionary.h"
+#include "proximity_info.h"
+#include "proximity_info_state.h"
+
+namespace latinime {
+void ProximityInfoState::initInputParams(
+        const ProximityInfo* proximityInfo, const int32_t* inputCodes, const int inputLength,
+        const int* xCoordinates, const int* yCoordinates) {
+    mProximityInfo = proximityInfo;
+    mHasTouchPositionCorrectionData = proximityInfo->hasTouchPositionCorrectionData();
+    mMostCommonKeyWidthSquare = proximityInfo->getMostCommonKeyWidthSquare();
+    mLocaleStr = proximityInfo->getLocaleStr();
+    mKeyCount = proximityInfo->getKeyCount();
+    mCellHeight = proximityInfo->getCellHeight();
+    mCellWidth = proximityInfo->getCellWidth();
+    mGridHeight = proximityInfo->getGridWidth();
+    mGridWidth = proximityInfo->getGridHeight();
+    const int normalizedSquaredDistancesLength =
+            MAX_PROXIMITY_CHARS_SIZE_INTERNAL * MAX_WORD_LENGTH_INTERNAL;
+    for (int i = 0; i < normalizedSquaredDistancesLength; ++i) {
+        mNormalizedSquaredDistances[i] = NOT_A_DISTANCE;
+    }
+
+    memset(mInputCodes, 0,
+            MAX_WORD_LENGTH_INTERNAL * MAX_PROXIMITY_CHARS_SIZE_INTERNAL * sizeof(mInputCodes[0]));
+
+    for (int i = 0; i < inputLength; ++i) {
+        const int32_t primaryKey = inputCodes[i];
+        const int x = xCoordinates[i];
+        const int y = yCoordinates[i];
+        int *proximities = &mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL];
+        mProximityInfo->calculateNearbyKeyCodes(x, y, primaryKey, proximities);
+    }
+
+    if (DEBUG_PROXIMITY_CHARS) {
+        for (int i = 0; i < inputLength; ++i) {
+            AKLOGI("---");
+            for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL; ++j) {
+                int icc = mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + j];
+                int icfjc = inputCodes[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + j];
+                icc += 0;
+                icfjc += 0;
+                AKLOGI("--- (%d)%c,%c", i, icc, icfjc); AKLOGI("--- A<%d>,B<%d>", icc, icfjc);
+            }
+        }
+    }
+    mInputXCoordinates = xCoordinates;
+    mInputYCoordinates = yCoordinates;
+    mTouchPositionCorrectionEnabled =
+            mHasTouchPositionCorrectionData && xCoordinates && yCoordinates;
+    mInputLength = inputLength;
+    for (int i = 0; i < inputLength; ++i) {
+        mPrimaryInputWord[i] = getPrimaryCharAt(i);
+    }
+    mPrimaryInputWord[inputLength] = 0;
+    if (DEBUG_PROXIMITY_CHARS) {
+        AKLOGI("--- initInputParams");
+    }
+    for (int i = 0; i < mInputLength; ++i) {
+        const int *proximityChars = getProximityCharsAt(i);
+        const int primaryKey = proximityChars[0];
+        const int x = xCoordinates[i];
+        const int y = yCoordinates[i];
+        if (DEBUG_PROXIMITY_CHARS) {
+            int a = x + y + primaryKey;
+            a += 0;
+            AKLOGI("--- Primary = %c, x = %d, y = %d", primaryKey, x, y);
+        }
+        for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL && proximityChars[j] > 0; ++j) {
+            const int currentChar = proximityChars[j];
+            const float squaredDistance =
+                    hasInputCoordinates() ? calculateNormalizedSquaredDistance(
+                            mProximityInfo->getKeyIndex(currentChar), i) :
+                            NOT_A_DISTANCE_FLOAT;
+            if (squaredDistance >= 0.0f) {
+                mNormalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + j] =
+                        (int) (squaredDistance * NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR);
+            } else {
+                mNormalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + j] =
+                        (j == 0) ? EQUIVALENT_CHAR_WITHOUT_DISTANCE_INFO :
+                                PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO;
+            }
+            if (DEBUG_PROXIMITY_CHARS) {
+                AKLOGI("--- Proximity (%d) = %c", j, currentChar);
+            }
+        }
+    }
+}
+
+float ProximityInfoState::calculateNormalizedSquaredDistance(
+        const int keyIndex, const int inputIndex) const {
+    if (keyIndex == NOT_AN_INDEX) {
+        return NOT_A_DISTANCE_FLOAT;
+    }
+    if (!mProximityInfo->hasSweetSpotData(keyIndex)) {
+        return NOT_A_DISTANCE_FLOAT;
+    }
+    if (NOT_A_COORDINATE == mInputXCoordinates[inputIndex]) {
+        return NOT_A_DISTANCE_FLOAT;
+    }
+    const float squaredDistance = calculateSquaredDistanceFromSweetSpotCenter(
+            keyIndex, inputIndex);
+    const float squaredRadius = square(mProximityInfo->getSweetSpotRadiiAt(keyIndex));
+    return squaredDistance / squaredRadius;
+}
+
+float ProximityInfoState::calculateSquaredDistanceFromSweetSpotCenter(
+        const int keyIndex, const int inputIndex) const {
+    const float sweetSpotCenterX = mProximityInfo->getSweetSpotCenterXAt(keyIndex);
+    const float sweetSpotCenterY = mProximityInfo->getSweetSpotCenterYAt(keyIndex);
+    const float inputX = (float)mInputXCoordinates[inputIndex];
+    const float inputY = (float)mInputYCoordinates[inputIndex];
+    return square(inputX - sweetSpotCenterX) + square(inputY - sweetSpotCenterY);
+}
+} // namespace latinime
diff --git a/native/jni/src/proximity_info_state.h b/native/jni/src/proximity_info_state.h
new file mode 100644
index 0000000..717871c
--- /dev/null
+++ b/native/jni/src/proximity_info_state.h
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2012 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_PROXIMITY_INFO_STATE_H
+#define LATINIME_PROXIMITY_INFO_STATE_H
+
+#include <assert.h>
+#include <stdint.h>
+#include <string>
+
+#include "additional_proximity_chars.h"
+#include "char_utils.h"
+#include "defines.h"
+
+namespace latinime {
+
+class ProximityInfo;
+
+class ProximityInfoState {
+ public:
+    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2 = 10;
+    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR =
+            1 << NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2;
+    // The upper limit of the char code in mCodeToKeyIndex
+    static const int MAX_CHAR_CODE = 127;
+    static const float NOT_A_DISTANCE_FLOAT = -1.0f;
+    static const int NOT_A_CODE = -1;
+
+    /////////////////////////////////////////
+    // Defined in proximity_info_state.cpp //
+    /////////////////////////////////////////
+    void initInputParams(
+            const ProximityInfo* proximityInfo, const int32_t* inputCodes, const int inputLength,
+            const int* xCoordinates, const int* yCoordinates);
+
+    /////////////////////////////////////////
+    // Defined here                        //
+    /////////////////////////////////////////
+    ProximityInfoState() {};
+    inline const int* getProximityCharsAt(const int index) const {
+        return mInputCodes + (index * MAX_PROXIMITY_CHARS_SIZE_INTERNAL);
+    }
+
+    inline unsigned short getPrimaryCharAt(const int index) const {
+        return getProximityCharsAt(index)[0];
+    }
+
+    inline bool existsCharInProximityAt(const int index, const int c) const {
+        const int *chars = getProximityCharsAt(index);
+        int i = 0;
+        while (chars[i] > 0 && i < MAX_PROXIMITY_CHARS_SIZE_INTERNAL) {
+            if (chars[i++] == c) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    inline bool existsAdjacentProximityChars(const int index) const {
+        if (index < 0 || index >= mInputLength) return false;
+        const int currentChar = getPrimaryCharAt(index);
+        const int leftIndex = index - 1;
+        if (leftIndex >= 0 && existsCharInProximityAt(leftIndex, currentChar)) {
+            return true;
+        }
+        const int rightIndex = index + 1;
+        if (rightIndex < mInputLength && existsCharInProximityAt(rightIndex, currentChar)) {
+            return true;
+        }
+        return false;
+    }
+
+    // In the following function, c is the current character of the dictionary word
+    // currently examined.
+    // currentChars is an array containing the keys close to the character the
+    // user actually typed at the same position. We want to see if c is in it: if so,
+    // then the word contains at that position a character close to what the user
+    // typed.
+    // What the user typed is actually the first character of the array.
+    // proximityIndex is a pointer to the variable where getMatchedProximityId returns
+    // the index of c in the proximity chars of the input index.
+    // Notice : accented characters do not have a proximity list, so they are alone
+    // in their list. The non-accented version of the character should be considered
+    // "close", but not the other keys close to the non-accented version.
+    inline ProximityType getMatchedProximityId(const int index,
+            const unsigned short c, const bool checkProximityChars, int *proximityIndex = 0) const {
+        const int *currentChars = getProximityCharsAt(index);
+        const int firstChar = currentChars[0];
+        const unsigned short baseLowerC = toBaseLowerCase(c);
+
+        // The first char in the array is what user typed. If it matches right away,
+        // that means the user typed that same char for this pos.
+        if (firstChar == baseLowerC || firstChar == c) {
+            return EQUIVALENT_CHAR;
+        }
+
+        if (!checkProximityChars) return UNRELATED_CHAR;
+
+        // If the non-accented, lowercased version of that first character matches c,
+        // then we have a non-accented version of the accented character the user
+        // typed. Treat it as a close char.
+        if (toBaseLowerCase(firstChar) == baseLowerC)
+            return NEAR_PROXIMITY_CHAR;
+
+        // Not an exact nor an accent-alike match: search the list of close keys
+        int j = 1;
+        while (j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+                && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+            const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
+            if (matched) {
+                if (proximityIndex) {
+                    *proximityIndex = j;
+                }
+                return NEAR_PROXIMITY_CHAR;
+            }
+            ++j;
+        }
+        if (j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+                && currentChars[j] == ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+            ++j;
+            while (j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+                    && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+                const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
+                if (matched) {
+                    if (proximityIndex) {
+                        *proximityIndex = j;
+                    }
+                    return ADDITIONAL_PROXIMITY_CHAR;
+                }
+                ++j;
+            }
+        }
+
+        // Was not included, signal this as an unrelated character.
+        return UNRELATED_CHAR;
+    }
+
+    inline int getNormalizedSquaredDistance(
+            const int inputIndex, const int proximityIndex) const {
+        return mNormalizedSquaredDistances[
+                inputIndex * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + proximityIndex];
+    }
+
+    inline const unsigned short* getPrimaryInputWord() const {
+        return mPrimaryInputWord;
+    }
+
+    inline bool touchPositionCorrectionEnabled() const {
+        return mTouchPositionCorrectionEnabled;
+    }
+
+ private:
+    DISALLOW_COPY_AND_ASSIGN(ProximityInfoState);
+    /////////////////////////////////////////
+    // Defined in proximity_info_state.cpp //
+    /////////////////////////////////////////
+    float calculateNormalizedSquaredDistance(const int keyIndex, const int inputIndex) const;
+
+    float calculateSquaredDistanceFromSweetSpotCenter(
+            const int keyIndex, const int inputIndex) const;
+
+    /////////////////////////////////////////
+    // Defined here                        //
+    /////////////////////////////////////////
+    inline float square(const float x) const { return x * x; }
+
+    bool hasInputCoordinates() const {
+        return mInputXCoordinates && mInputYCoordinates;
+    }
+
+    bool sameAsTyped(const unsigned short *word, int length) const {
+        if (length != mInputLength) {
+            return false;
+        }
+        const int *inputCodes = mInputCodes;
+        while (length--) {
+            if ((unsigned int) *inputCodes != (unsigned int) *word) {
+                return false;
+            }
+            inputCodes += MAX_PROXIMITY_CHARS_SIZE_INTERNAL;
+            word++;
+        }
+        return true;
+    }
+
+    // const
+    const ProximityInfo *mProximityInfo;
+    bool mHasTouchPositionCorrectionData;
+    int mMostCommonKeyWidthSquare;
+    std::string mLocaleStr;
+    int mKeyCount;
+    int mCellHeight;
+    int mCellWidth;
+    int mGridHeight;
+    int mGridWidth;
+
+    const int *mInputXCoordinates;
+    const int *mInputYCoordinates;
+    bool mTouchPositionCorrectionEnabled;
+    int32_t mInputCodes[MAX_PROXIMITY_CHARS_SIZE_INTERNAL * MAX_WORD_LENGTH_INTERNAL];
+    int mNormalizedSquaredDistances[MAX_PROXIMITY_CHARS_SIZE_INTERNAL * MAX_WORD_LENGTH_INTERNAL];
+    int mInputLength;
+    unsigned short mPrimaryInputWord[MAX_WORD_LENGTH_INTERNAL];
+};
+
+} // namespace latinime
+
+#endif // LATINIME_PROXIMITY_INFO_STATE_H
diff --git a/native/jni/src/terminal_attributes.h b/native/jni/src/terminal_attributes.h
index 9a803cc..c712f50 100644
--- a/native/jni/src/terminal_attributes.h
+++ b/native/jni/src/terminal_attributes.h
@@ -62,6 +62,7 @@
     };
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(TerminalAttributes);
     const uint8_t* const mDict;
     const uint8_t mFlags;
     const int mStartPos;
diff --git a/native/jni/src/unigram_dictionary.cpp b/native/jni/src/unigram_dictionary.cpp
index ea9f11b..3417d2b 100644
--- a/native/jni/src/unigram_dictionary.cpp
+++ b/native/jni/src/unigram_dictionary.cpp
@@ -103,7 +103,7 @@
         const bool useFullEditDistance, const int *codesSrc,
         const int codesRemain, const int currentDepth, int *codesDest, Correction *correction,
         WordsPriorityQueuePool *queuePool,
-        const digraph_t* const digraphs, const unsigned int digraphsSize) {
+        const digraph_t* const digraphs, const unsigned int digraphsSize) const {
 
     const int startIndex = codesDest - codesBuffer;
     if (currentDepth < MAX_DIGRAPH_SEARCH_DEPTH) {
@@ -170,14 +170,15 @@
 // bigramFilter is a bloom filter for fast rejection: see functions setInFilter and isInFilter
 // in bigram_dictionary.cpp
 int UnigramDictionary::getSuggestions(ProximityInfo *proximityInfo,
-        WordsPriorityQueuePool *queuePool, Correction *correction, const int *xcoordinates,
+        const int *xcoordinates,
         const int *ycoordinates, const int *codes, const int codesSize,
         const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
-        const bool useFullEditDistance, unsigned short *outWords, int *frequencies) {
+        const bool useFullEditDistance, unsigned short *outWords, int *frequencies) const {
 
-    queuePool->clearAll();
-    Correction* masterCorrection = correction;
-    correction->resetCorrection();
+    WordsPriorityQueuePool queuePool(MAX_WORDS, SUB_QUEUE_MAX_WORDS, MAX_WORD_LENGTH);
+    queuePool.clearAll();
+    Correction masterCorrection;
+    masterCorrection.resetCorrection();
     if (BinaryFormat::REQUIRES_GERMAN_UMLAUT_PROCESSING & FLAGS)
     { // Incrementally tune the word and try all possibilities
         int codesBuffer[getCodesBufferSize(codes, codesSize)];
@@ -185,8 +186,8 @@
         int yCoordinatesBuffer[codesSize];
         getWordWithDigraphSuggestionsRec(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
                 xCoordinatesBuffer, yCoordinatesBuffer, codesSize, bigramMap, bigramFilter,
-                useFullEditDistance, codes, codesSize, 0, codesBuffer, masterCorrection,
-                queuePool, GERMAN_UMLAUT_DIGRAPHS,
+                useFullEditDistance, codes, codesSize, 0, codesBuffer, &masterCorrection,
+                &queuePool, GERMAN_UMLAUT_DIGRAPHS,
                 sizeof(GERMAN_UMLAUT_DIGRAPHS) / sizeof(GERMAN_UMLAUT_DIGRAPHS[0]));
     } else if (BinaryFormat::REQUIRES_FRENCH_LIGATURES_PROCESSING & FLAGS) {
         int codesBuffer[getCodesBufferSize(codes, codesSize)];
@@ -194,28 +195,28 @@
         int yCoordinatesBuffer[codesSize];
         getWordWithDigraphSuggestionsRec(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
                 xCoordinatesBuffer, yCoordinatesBuffer, codesSize, bigramMap, bigramFilter,
-                useFullEditDistance, codes, codesSize, 0, codesBuffer, masterCorrection,
-                queuePool, FRENCH_LIGATURES_DIGRAPHS,
+                useFullEditDistance, codes, codesSize, 0, codesBuffer, &masterCorrection,
+                &queuePool, FRENCH_LIGATURES_DIGRAPHS,
                 sizeof(FRENCH_LIGATURES_DIGRAPHS) / sizeof(FRENCH_LIGATURES_DIGRAPHS[0]));
     } else { // Normal processing
         getWordSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, codesSize,
-                bigramMap, bigramFilter, useFullEditDistance, masterCorrection, queuePool);
+                bigramMap, bigramFilter, useFullEditDistance, &masterCorrection, &queuePool);
     }
 
     PROF_START(20);
     if (DEBUG_DICT) {
-        float ns = queuePool->getMasterQueue()->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), codesSize, 0, 0, 0);
+        float ns = queuePool.getMasterQueue()->getHighestNormalizedScore(
+                masterCorrection.getPrimaryInputWord(), codesSize, 0, 0, 0);
         ns += 0;
         AKLOGI("Max normalized score = %f", ns);
     }
     const int suggestedWordsCount =
-            queuePool->getMasterQueue()->outputSuggestions(
-                    proximityInfo->getPrimaryInputWord(), codesSize, frequencies, outWords);
+            queuePool.getMasterQueue()->outputSuggestions(
+                    masterCorrection.getPrimaryInputWord(), codesSize, frequencies, outWords);
 
     if (DEBUG_DICT) {
-        float ns = queuePool->getMasterQueue()->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), codesSize, 0, 0, 0);
+        float ns = queuePool.getMasterQueue()->getHighestNormalizedScore(
+                masterCorrection.getPrimaryInputWord(), codesSize, 0, 0, 0);
         ns += 0;
         AKLOGI("Returning %d words", suggestedWordsCount);
         /// Print the returned words
@@ -235,7 +236,8 @@
 void UnigramDictionary::getWordSuggestions(ProximityInfo *proximityInfo,
         const int *xcoordinates, const int *ycoordinates, const int *codes,
         const int inputLength, const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
-        const bool useFullEditDistance, Correction *correction, WordsPriorityQueuePool *queuePool) {
+        const bool useFullEditDistance, Correction *correction,
+        WordsPriorityQueuePool *queuePool) const {
 
     PROF_OPEN;
     PROF_START(0);
@@ -259,7 +261,7 @@
     WordsPriorityQueue* masterQueue = queuePool->getMasterQueue();
     if (masterQueue->size() > 0) {
         float nsForMaster = masterQueue->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), inputLength, 0, 0, 0);
+                correction->getPrimaryInputWord(), inputLength, 0, 0, 0);
         hasAutoCorrectionCandidate = (nsForMaster > START_TWO_WORDS_CORRECTION_THRESHOLD);
     }
     PROF_END(4);
@@ -288,11 +290,11 @@
                 const unsigned short* word = sw->mWord;
                 const int wordLength = sw->mWordLength;
                 float ns = Correction::RankingAlgorithm::calcNormalizedScore(
-                        proximityInfo->getPrimaryInputWord(), i, word, wordLength, score);
+                        correction->getPrimaryInputWord(), i, word, wordLength, score);
                 ns += 0;
                 AKLOGI("--- TOP SUB WORDS for %d --- %d %f [%d]", i, score, ns,
                         (ns > TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD));
-                DUMP_WORD(proximityInfo->getPrimaryInputWord(), i);
+                DUMP_WORD(correction->getPrimaryInputWord(), i);
                 DUMP_WORD(word, wordLength);
             }
         }
@@ -300,12 +302,13 @@
 }
 
 void UnigramDictionary::initSuggestions(ProximityInfo *proximityInfo, const int *xCoordinates,
-        const int *yCoordinates, const int *codes, const int inputLength, Correction *correction) {
+        const int *yCoordinates, const int *codes, const int inputLength,
+        Correction *correction) const {
     if (DEBUG_DICT) {
         AKLOGI("initSuggest");
         DUMP_WORD_INT(codes, inputLength);
     }
-    proximityInfo->setInputParams(codes, inputLength, xCoordinates, yCoordinates);
+    correction->initInputParams(proximityInfo, codes, inputLength, xCoordinates, yCoordinates);
     const int maxDepth = min(inputLength * MAX_DEPTH_MULTIPLIER, MAX_WORD_LENGTH);
     correction->initCorrection(proximityInfo, inputLength, maxDepth);
 }
@@ -317,7 +320,7 @@
         const int *xcoordinates, const int *ycoordinates, const int *codes,
         const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
         const bool useFullEditDistance, const int inputLength,
-        Correction *correction, WordsPriorityQueuePool *queuePool) {
+        Correction *correction, WordsPriorityQueuePool *queuePool) const {
     initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, inputLength, correction);
     getSuggestionCandidates(useFullEditDistance, inputLength, bigramMap, bigramFilter, correction,
             queuePool, true /* doAutoCompletion */, DEFAULT_MAX_ERRORS, FIRST_WORD_INDEX);
@@ -326,7 +329,7 @@
 void UnigramDictionary::getSuggestionCandidates(const bool useFullEditDistance,
         const int inputLength, const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
         Correction *correction, WordsPriorityQueuePool *queuePool,
-        const bool doAutoCompletion, const int maxErrors, const int currentWordIndex) {
+        const bool doAutoCompletion, const int maxErrors, const int currentWordIndex) const {
     uint8_t totalTraverseCount = correction->pushAndGetTotalTraverseCount();
     if (DEBUG_DICT) {
         AKLOGI("Traverse count %d", totalTraverseCount);
@@ -374,7 +377,7 @@
 inline void UnigramDictionary::onTerminal(const int probability,
         const TerminalAttributes& terminalAttributes, Correction *correction,
         WordsPriorityQueuePool *queuePool, const bool addToMasterQueue,
-        const int currentWordIndex) {
+        const int currentWordIndex) const {
     const int inputIndex = correction->getInputIndex();
     const bool addToSubQueue = inputIndex < SUB_QUEUE_MAX_COUNT;
 
@@ -430,7 +433,7 @@
         const bool hasAutoCorrectionCandidate, const int currentWordIndex,
         const int inputWordStartPos, const int inputWordLength,
         const int outputWordStartPos, const bool isSpaceProximity, int *freqArray,
-        int*wordLengthArray, unsigned short* outputWord, int *outputWordLength) {
+        int*wordLengthArray, unsigned short* outputWord, int *outputWordLength) const {
     if (inputWordLength > MULTIPLE_WORDS_SUGGESTION_MAX_WORD_LENGTH) {
         return FLAG_MULTIPLE_SUGGEST_ABORT;
     }
@@ -479,11 +482,12 @@
     initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes,
             inputLength, correction);
 
+    unsigned short word[MAX_WORD_LENGTH_INTERNAL];
     int freq = getMostFrequentWordLike(
-            inputWordStartPos, inputWordLength, proximityInfo, mWord);
+            inputWordStartPos, inputWordLength, correction, word);
     if (freq > 0) {
         nextWordLength = inputWordLength;
-        tempOutputWord = mWord;
+        tempOutputWord = word;
     } else if (!hasAutoCorrectionCandidate) {
         if (inputWordStartPos > 0) {
             const int offset = inputWordStartPos;
@@ -510,7 +514,7 @@
         }
         int score = 0;
         const float ns = queue->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), inputWordLength,
+                correction->getPrimaryInputWord(), inputWordLength,
                 &tempOutputWord, &score, &nextWordLength);
         if (DEBUG_DICT) {
             AKLOGI("NS(%d) = %f, Score = %d", currentWordIndex, ns, score);
@@ -577,7 +581,7 @@
         Correction *correction, WordsPriorityQueuePool* queuePool,
         const bool hasAutoCorrectionCandidate, const int startInputPos, const int startWordIndex,
         const int outputWordLength, int *freqArray, int* wordLengthArray,
-        unsigned short* outputWord) {
+        unsigned short* outputWord) const {
     if (startWordIndex >= (MULTIPLE_WORDS_SUGGESTION_MAX_WORDS - 1)) {
         // Return if the last word index
         return;
@@ -656,7 +660,7 @@
         const int *xcoordinates, const int *ycoordinates, const int *codes,
         const bool useFullEditDistance, const int inputLength,
         Correction *correction, WordsPriorityQueuePool* queuePool,
-        const bool hasAutoCorrectionCandidate) {
+        const bool hasAutoCorrectionCandidate) const {
     if (inputLength >= MAX_WORD_LENGTH) return;
     if (DEBUG_DICT) {
         AKLOGI("--- Suggest multiple words");
@@ -678,11 +682,11 @@
 // Wrapper for getMostFrequentWordLikeInner, which matches it to the previous
 // interface.
 inline int UnigramDictionary::getMostFrequentWordLike(const int startInputIndex,
-        const int inputLength, ProximityInfo *proximityInfo, unsigned short *word) {
+        const int inputLength, Correction *correction, unsigned short *word) const {
     uint16_t inWord[inputLength];
 
     for (int i = 0; i < inputLength; ++i) {
-        inWord[i] = (uint16_t)proximityInfo->getPrimaryCharAt(startInputIndex + i);
+        inWord[i] = (uint16_t)correction->getPrimaryCharAt(startInputIndex + i);
     }
     return getMostFrequentWordLikeInner(inWord, inputLength, word);
 }
@@ -751,21 +755,24 @@
 // Will find the highest frequency of the words like the one passed as an argument,
 // that is, everything that only differs by case/accents.
 int UnigramDictionary::getMostFrequentWordLikeInner(const uint16_t * const inWord,
-        const int length, short unsigned int* outWord) {
+        const int length, short unsigned int* outWord) const {
     int32_t newWord[MAX_WORD_LENGTH_INTERNAL];
     int depth = 0;
     int maxFreq = -1;
     const uint8_t* const root = DICT_ROOT;
+    int stackChildCount[MAX_WORD_LENGTH_INTERNAL];
+    int stackInputIndex[MAX_WORD_LENGTH_INTERNAL];
+    int stackSiblingPos[MAX_WORD_LENGTH_INTERNAL];
 
     int startPos = 0;
-    mStackChildCount[0] = BinaryFormat::getGroupCountAndForwardPointer(root, &startPos);
-    mStackInputIndex[0] = 0;
-    mStackSiblingPos[0] = startPos;
+    stackChildCount[0] = BinaryFormat::getGroupCountAndForwardPointer(root, &startPos);
+    stackInputIndex[0] = 0;
+    stackSiblingPos[0] = startPos;
     while (depth >= 0) {
-        const int charGroupCount = mStackChildCount[depth];
-        int pos = mStackSiblingPos[depth];
+        const int charGroupCount = stackChildCount[depth];
+        int pos = stackSiblingPos[depth];
         for (int charGroupIndex = charGroupCount - 1; charGroupIndex >= 0; --charGroupIndex) {
-            int inputIndex = mStackInputIndex[depth];
+            int inputIndex = stackInputIndex[depth];
             const uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
             // Test whether all chars in this group match with the word we are searching for. If so,
             // we want to traverse its children (or if the length match, evaluate its frequency).
@@ -785,15 +792,15 @@
             // anyway, so don't traverse unless inputIndex < length.
             if (isAlike && (-1 != childrenNodePos) && (inputIndex < length)) {
                 // Save position for this depth, to get back to this once children are done
-                mStackChildCount[depth] = charGroupIndex;
-                mStackSiblingPos[depth] = siblingPos;
+                stackChildCount[depth] = charGroupIndex;
+                stackSiblingPos[depth] = siblingPos;
                 // Prepare stack values for next depth
                 ++depth;
                 int childrenPos = childrenNodePos;
-                mStackChildCount[depth] =
+                stackChildCount[depth] =
                         BinaryFormat::getGroupCountAndForwardPointer(root, &childrenPos);
-                mStackSiblingPos[depth] = childrenPos;
-                mStackInputIndex[depth] = inputIndex;
+                stackSiblingPos[depth] = childrenPos;
+                stackInputIndex[depth] = inputIndex;
                 pos = childrenPos;
                 // Go to the next depth level.
                 ++depth;
@@ -848,7 +855,7 @@
 inline bool UnigramDictionary::processCurrentNode(const int initialPos,
         const std::map<int, int> *bigramMap, const uint8_t *bigramFilter, Correction *correction,
         int *newCount, int *newChildrenPosition, int *nextSiblingPosition,
-        WordsPriorityQueuePool *queuePool, const int currentWordIndex) {
+        WordsPriorityQueuePool *queuePool, const int currentWordIndex) const {
     if (DEBUG_DICT) {
         correction->checkState();
     }
diff --git a/native/jni/src/unigram_dictionary.h b/native/jni/src/unigram_dictionary.h
index a1a8299..8352c54 100644
--- a/native/jni/src/unigram_dictionary.h
+++ b/native/jni/src/unigram_dictionary.h
@@ -77,19 +77,20 @@
             int fullWordMultiplier, int maxWordLength, int maxWords, const unsigned int flags);
     int getFrequency(const int32_t* const inWord, const int length) const;
     int getBigramPosition(int pos, unsigned short *word, int offset, int length) const;
-    int getSuggestions(ProximityInfo *proximityInfo, WordsPriorityQueuePool *queuePool,
-            Correction *correction, const int *xcoordinates, const int *ycoordinates,
+    int getSuggestions(
+            ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates,
             const int *codes, const int codesSize, const std::map<int, int> *bigramMap,
             const uint8_t *bigramFilter, const bool useFullEditDistance, unsigned short *outWords,
-            int *frequencies);
+            int *frequencies) const;
     virtual ~UnigramDictionary();
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(UnigramDictionary);
     void getWordSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
             const int *ycoordinates, const int *codes, const int inputLength,
             const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
             const bool useFullEditDistance, Correction *correction,
-            WordsPriorityQueuePool *queuePool);
+            WordsPriorityQueuePool *queuePool) const;
     int getDigraphReplacement(const int *codes, const int i, const int codesSize,
             const digraph_t* const digraphs, const unsigned int digraphsSize) const;
     void getWordWithDigraphSuggestionsRec(ProximityInfo *proximityInfo,
@@ -99,37 +100,36 @@
         const bool useFullEditDistance, const int* codesSrc, const int codesRemain,
         const int currentDepth, int* codesDest, Correction *correction,
         WordsPriorityQueuePool* queuePool, const digraph_t* const digraphs,
-        const unsigned int digraphsSize);
+        const unsigned int digraphsSize) const;
     void initSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
-            const int *ycoordinates, const int *codes, const int codesSize, Correction *correction);
+            const int *ycoordinates, const int *codes, const int codesSize,
+            Correction *correction) const;
     void getOneWordSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
             const int *ycoordinates, const int *codes, const std::map<int, int> *bigramMap,
             const uint8_t *bigramFilter, const bool useFullEditDistance, const int inputLength,
-            Correction *correction, WordsPriorityQueuePool* queuePool);
+            Correction *correction, WordsPriorityQueuePool* queuePool) const;
     void getSuggestionCandidates(
             const bool useFullEditDistance, const int inputLength,
             const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
             Correction *correction, WordsPriorityQueuePool* queuePool, const bool doAutoCompletion,
-            const int maxErrors, const int currentWordIndex);
+            const int maxErrors, const int currentWordIndex) const;
     void getSplitMultipleWordsSuggestions(ProximityInfo *proximityInfo,
             const int *xcoordinates, const int *ycoordinates, const int *codes,
             const bool useFullEditDistance, const int inputLength,
             Correction *correction, WordsPriorityQueuePool* queuePool,
-            const bool hasAutoCorrectionCandidate);
+            const bool hasAutoCorrectionCandidate) const;
     void onTerminal(const int freq, const TerminalAttributes& terminalAttributes,
             Correction *correction, WordsPriorityQueuePool *queuePool, const bool addToMasterQueue,
-            const int currentWordIndex);
-    bool needsToSkipCurrentNode(const unsigned short c,
-            const int inputIndex, const int skipPos, const int depth);
+            const int currentWordIndex) const;
     // Process a node by considering proximity, missing and excessive character
     bool processCurrentNode(const int initialPos, const std::map<int, int> *bigramMap,
             const uint8_t *bigramFilter, Correction *correction, int *newCount,
             int *newChildPosition, int *nextSiblingPosition, WordsPriorityQueuePool *queuePool,
-            const int currentWordIndex);
+            const int currentWordIndex) const;
     int getMostFrequentWordLike(const int startInputIndex, const int inputLength,
-            ProximityInfo *proximityInfo, unsigned short *word);
+            Correction *correction, unsigned short *word) const;
     int getMostFrequentWordLikeInner(const uint16_t* const inWord, const int length,
-            short unsigned int *outWord);
+            short unsigned int *outWord) const;
     int getSubStringSuggestion(
             ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates,
             const int *codes, const bool useFullEditDistance, Correction *correction,
@@ -137,14 +137,14 @@
             const bool hasAutoCorrectionCandidate, const int currentWordIndex,
             const int inputWordStartPos, const int inputWordLength,
             const int outputWordStartPos, const bool isSpaceProximity, int *freqArray,
-            int *wordLengthArray, unsigned short* outputWord, int *outputWordLength);
+            int *wordLengthArray, unsigned short* outputWord, int *outputWordLength) const;
     void getMultiWordsSuggestionRec(ProximityInfo *proximityInfo,
             const int *xcoordinates, const int *ycoordinates, const int *codes,
             const bool useFullEditDistance, const int inputLength,
             Correction *correction, WordsPriorityQueuePool* queuePool,
             const bool hasAutoCorrectionCandidate, const int startPos, const int startWordIndex,
             const int outputWordLength, int *freqArray, int* wordLengthArray,
-            unsigned short* outputWord);
+            unsigned short* outputWord) const;
 
     const uint8_t* const DICT_ROOT;
     const int MAX_WORD_LENGTH;
@@ -158,12 +158,6 @@
 
     static const digraph_t GERMAN_UMLAUT_DIGRAPHS[];
     static const digraph_t FRENCH_LIGATURES_DIGRAPHS[];
-
-    // Still bundled members
-    unsigned short mWord[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
-    int mStackChildCount[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
-    int mStackInputIndex[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
-    int mStackSiblingPos[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
 };
 } // namespace latinime
 
diff --git a/native/jni/src/words_priority_queue.h b/native/jni/src/words_priority_queue.h
index 7629251..9c6d28d 100644
--- a/native/jni/src/words_priority_queue.h
+++ b/native/jni/src/words_priority_queue.h
@@ -182,6 +182,7 @@
     }
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(WordsPriorityQueue);
     struct wordComparator {
         bool operator ()(SuggestedWord * left, SuggestedWord * right) {
             return left->mScore > right->mScore;
diff --git a/native/jni/src/words_priority_queue_pool.h b/native/jni/src/words_priority_queue_pool.h
index 210b5a8..b4e2bed 100644
--- a/native/jni/src/words_priority_queue_pool.h
+++ b/native/jni/src/words_priority_queue_pool.h
@@ -85,6 +85,7 @@
     }
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(WordsPriorityQueuePool);
     WordsPriorityQueue* mMasterQueue;
     WordsPriorityQueue* mSubQueues[SUB_QUEUE_MAX_COUNT * MULTIPLE_WORDS_SUGGESTION_MAX_WORDS];
     char mMasterQueueBuf[sizeof(WordsPriorityQueue)];
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
index fa067f4..3dc4543 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
@@ -16,25 +16,27 @@
 
 package com.android.inputmethod.keyboard.internal;
 
-import android.test.AndroidTestCase;
+import android.app.Instrumentation;
+import android.test.InstrumentationTestCase;
 
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Locale;
 
-public class KeySpecParserCsvTests extends AndroidTestCase {
+public class KeySpecParserCsvTests extends InstrumentationTestCase {
     private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
 
+        final Instrumentation instrumentation = getInstrumentation();
         mTextsSet.setLanguage(Locale.ENGLISH.getLanguage());
-        mTextsSet.loadStringResources(getContext());
+        mTextsSet.loadStringResources(instrumentation.getTargetContext());
         final String[] testResourceNames = getAllResourceIdNames(
                 com.android.inputmethod.latin.tests.R.string.class);
-        mTextsSet.loadStringResourcesInternal(getTestContext(),
+        mTextsSet.loadStringResourcesInternal(instrumentation.getContext(),
                 testResourceNames,
                 com.android.inputmethod.latin.tests.R.string.empty_string);
     }
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index eb47fd5..c672d51 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -130,11 +130,11 @@
                 (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         final ViewGroup vg = new FrameLayout(getContext());
         final View inputView = inflater.inflate(R.layout.input_view, vg);
+        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
         mLatinIME.setInputView(inputView);
         mLatinIME.onBindInput();
         mLatinIME.onCreateInputView();
         mLatinIME.onStartInputView(ei, false);
-        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
         mInputConnection = ic;
         mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
         changeLanguage("en_US");
diff --git a/tests/src/com/android/inputmethod/latin/PunctuationTests.java b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
index e1d4c46..0eb3ba4 100644
--- a/tests/src/com/android/inputmethod/latin/PunctuationTests.java
+++ b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
@@ -27,7 +27,7 @@
         final String PUNCTUATION_FROM_STRIP = "!";
         final String EXPECTED_RESULT = "this!! ";
         final boolean defaultNextWordPredictionOption =
-                mLatinIME.getResources().getBoolean(R.bool.config_default_next_word_suggestions);
+                mLatinIME.getResources().getBoolean(R.bool.config_default_next_word_prediction);
         final boolean previousNextWordPredictionOption =
                 setBooleanPreference(NEXT_WORD_PREDICTION_OPTION, false,
                         defaultNextWordPredictionOption);
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
new file mode 100644
index 0000000..9ce581d
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2010 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.AndroidTestCase;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+
+import com.android.inputmethod.latin.RichInputConnection.Range;
+
+public class RichInputConnectionTests extends AndroidTestCase {
+
+    // The following is meant to be a reasonable default for
+    // the "word_separators" resource.
+    private static final String sSeparators = ".,:;!?-";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    private class MockConnection extends InputConnectionWrapper {
+        final String mTextBefore;
+        final String mTextAfter;
+        final ExtractedText mExtractedText;
+
+        public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) {
+            super(null, false);
+            mTextBefore = textBefore;
+            mTextAfter = textAfter;
+            mExtractedText = extractedText;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int)
+         */
+        @Override
+        public CharSequence getTextBeforeCursor(int n, int flags) {
+            return mTextBefore;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int)
+         */
+        @Override
+        public CharSequence getTextAfterCursor(int n, int flags) {
+            return mTextAfter;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText(
+         *         ExtractedTextRequest, int)
+         */
+        @Override
+        public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+            return mExtractedText;
+        }
+
+        @Override
+        public boolean beginBatchEdit() {
+            return true;
+        }
+
+        @Override
+        public boolean endBatchEdit() {
+            return true;
+        }
+    }
+
+    /************************** Tests ************************/
+
+    /**
+     * Test for getting previous word (for bigram suggestions)
+     */
+    public void testGetPreviousWord() {
+        // If one of the following cases breaks, the bigram suggestions won't work.
+        assertEquals(RichInputConnection.getPreviousWord("abc def", sSeparators), "abc");
+        assertNull(RichInputConnection.getPreviousWord("abc", sSeparators));
+        assertNull(RichInputConnection.getPreviousWord("abc. def", sSeparators));
+
+        // The following tests reflect the current behavior of the function
+        // RichInputConnection#getPreviousWord.
+        // TODO: However at this time, the code does never go
+        // into such a path, so it should be safe to change the behavior of
+        // this function if needed - especially since it does not seem very
+        // logical. These tests are just there to catch any unintentional
+        // changes in the behavior of the RichInputConnection#getPreviousWord method.
+        assertEquals(RichInputConnection.getPreviousWord("abc def ", sSeparators), "abc");
+        assertEquals(RichInputConnection.getPreviousWord("abc def.", sSeparators), "abc");
+        assertEquals(RichInputConnection.getPreviousWord("abc def .", sSeparators), "def");
+        assertNull(RichInputConnection.getPreviousWord("abc ", sSeparators));
+    }
+
+    /**
+     * Test for getting the word before the cursor (for bigram)
+     */
+    public void testGetThisWord() {
+        assertEquals(RichInputConnection.getThisWord("abc def", sSeparators), "def");
+        assertEquals(RichInputConnection.getThisWord("abc def ", sSeparators), "def");
+        assertNull(RichInputConnection.getThisWord("abc def.", sSeparators));
+        assertNull(RichInputConnection.getThisWord("abc def .", sSeparators));
+    }
+
+    /**
+     * Test logic in getting the word range at the cursor.
+     */
+    public void testGetWordRangeAtCursor() {
+        ExtractedText et = new ExtractedText();
+        final RichInputConnection ic = new RichInputConnection();
+        InputConnection mockConnection;
+        mockConnection = new MockConnection("word wo", "rd", et);
+        et.startOffset = 0;
+        et.selectionStart = 7;
+        Range r;
+
+        ic.beginBatchEdit(mockConnection);
+        // basic case
+        r = ic.getWordRangeAtCursor(" ", 0);
+        assertEquals("word", r.mWord);
+
+        // more than one word
+        r = ic.getWordRangeAtCursor(" ", 1);
+        assertEquals("word word", r.mWord);
+        ic.endBatchEdit();
+
+        // tab character instead of space
+        mockConnection = new MockConnection("one\tword\two", "rd", et);
+        ic.beginBatchEdit(mockConnection);
+        r = ic.getWordRangeAtCursor("\t", 1);
+        ic.endBatchEdit();
+        assertEquals("word\tword", r.mWord);
+
+        // only one word doesn't go too far
+        mockConnection = new MockConnection("one\tword\two", "rd", et);
+        ic.beginBatchEdit(mockConnection);
+        r = ic.getWordRangeAtCursor("\t", 1);
+        ic.endBatchEdit();
+        assertEquals("word\tword", r.mWord);
+
+        // tab or space
+        mockConnection = new MockConnection("one word\two", "rd", et);
+        ic.beginBatchEdit(mockConnection);
+        r = ic.getWordRangeAtCursor(" \t", 1);
+        ic.endBatchEdit();
+        assertEquals("word\tword", r.mWord);
+
+        // tab or space multiword
+        mockConnection = new MockConnection("one word\two", "rd", et);
+        ic.beginBatchEdit(mockConnection);
+        r = ic.getWordRangeAtCursor(" \t", 2);
+        ic.endBatchEdit();
+        assertEquals("one word\tword", r.mWord);
+
+        // splitting on supplementary character
+        final String supplementaryChar = "\uD840\uDC8A";
+        mockConnection = new MockConnection("one word" + supplementaryChar + "wo", "rd", et);
+        ic.beginBatchEdit(mockConnection);
+        r = ic.getWordRangeAtCursor(supplementaryChar, 0);
+        ic.endBatchEdit();
+        assertEquals("word", r.mWord);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/UtilsTests.java b/tests/src/com/android/inputmethod/latin/UtilsTests.java
deleted file mode 100644
index 2ef4e2f..0000000
--- a/tests/src/com/android/inputmethod/latin/UtilsTests.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2010,2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.test.AndroidTestCase;
-
-public class UtilsTests extends AndroidTestCase {
-
-    // The following is meant to be a reasonable default for
-    // the "word_separators" resource.
-    private static final String sSeparators = ".,:;!?-";
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-    }
-
-    /************************** Tests ************************/
-
-    /**
-     * Test for getting previous word (for bigram suggestions)
-     */
-    public void testGetPreviousWord() {
-        // If one of the following cases breaks, the bigram suggestions won't work.
-        assertEquals(EditingUtils.getPreviousWord("abc def", sSeparators), "abc");
-        assertNull(EditingUtils.getPreviousWord("abc", sSeparators));
-        assertNull(EditingUtils.getPreviousWord("abc. def", sSeparators));
-
-        // The following tests reflect the current behavior of the function
-        // EditingUtils#getPreviousWord.
-        // TODO: However at this time, the code does never go
-        // into such a path, so it should be safe to change the behavior of
-        // this function if needed - especially since it does not seem very
-        // logical. These tests are just there to catch any unintentional
-        // changes in the behavior of the EditingUtils#getPreviousWord method.
-        assertEquals(EditingUtils.getPreviousWord("abc def ", sSeparators), "abc");
-        assertEquals(EditingUtils.getPreviousWord("abc def.", sSeparators), "abc");
-        assertEquals(EditingUtils.getPreviousWord("abc def .", sSeparators), "def");
-        assertNull(EditingUtils.getPreviousWord("abc ", sSeparators));
-    }
-
-    /**
-     * Test for getting the word before the cursor (for bigram)
-     */
-    public void testGetThisWord() {
-        assertEquals(EditingUtils.getThisWord("abc def", sSeparators), "def");
-        assertEquals(EditingUtils.getThisWord("abc def ", sSeparators), "def");
-        assertNull(EditingUtils.getThisWord("abc def.", sSeparators));
-        assertNull(EditingUtils.getThisWord("abc def .", sSeparators));
-    }
-}
diff --git a/tools/dicttool/Android.mk b/tools/dicttool/Android.mk
new file mode 100644
index 0000000..9e8dbe0
--- /dev/null
+++ b/tools/dicttool/Android.mk
@@ -0,0 +1,25 @@
+#
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_JAR_MANIFEST := etc/manifest.txt
+LOCAL_MODULE := dicttool
+LOCAL_MODULE_TAGS := eng
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+include $(LOCAL_PATH)/etc/Android.mk
diff --git a/tools/dicttool/etc/Android.mk b/tools/dicttool/etc/Android.mk
new file mode 100644
index 0000000..03d4a96
--- /dev/null
+++ b/tools/dicttool/etc/Android.mk
@@ -0,0 +1,20 @@
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := eng
+LOCAL_PREBUILT_EXECUTABLES := dicttool
+include $(BUILD_HOST_PREBUILT)
diff --git a/tools/dicttool/etc/dicttool b/tools/dicttool/etc/dicttool
new file mode 100755
index 0000000..8a39694
--- /dev/null
+++ b/tools/dicttool/etc/dicttool
@@ -0,0 +1,62 @@
+#!/bin/sh
+# Copyright 2011, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+    newProg=`/bin/ls -ld "${prog}"`
+    newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+    if expr "x${newProg}" : 'x/' >/dev/null; then
+        prog="${newProg}"
+    else
+        progdir=`dirname "${prog}"`
+        prog="${progdir}/${newProg}"
+    fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=dicttool.jar
+frameworkdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/tools/lib
+    libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/framework
+    libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    echo `basename "$prog"`": can't find $jarfile"
+    exit 1
+fi
+
+if [ "$OSTYPE" = "cygwin" ] ; then
+    jarpath=`cygpath -w  "$frameworkdir/$jarfile"`
+    progdir=`cygpath -w  "$progdir"`
+else
+    jarpath="$frameworkdir/$jarfile"
+fi
+
+# might need more memory, e.g. -Xmx128M
+exec java -ea -jar "$jarpath" "$@"
diff --git a/tools/dicttool/etc/manifest.txt b/tools/dicttool/etc/manifest.txt
new file mode 100644
index 0000000..67c8521
--- /dev/null
+++ b/tools/dicttool/etc/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.inputmethod.latin.dicttool.Dicttool
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java
new file mode 100644
index 0000000..307f596
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright (C) 2012 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.dicttool;
+
+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.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+public class Compress {
+
+    private static OutputStream getCompressedStream(final OutputStream out)
+        throws java.io.IOException {
+        return new GZIPOutputStream(out);
+    }
+
+    private static InputStream getUncompressedStream(final InputStream in) throws IOException {
+        return new GZIPInputStream(in);
+    }
+
+    public static void copy(final InputStream input, final OutputStream output) throws IOException {
+        final byte[] buffer = new byte[1000];
+        for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
+            output.write(buffer, 0, readBytes);
+        input.close();
+        output.close();
+    }
+
+    static public class Compressor extends Dicttool.Command {
+        public static final String COMMAND = "compress";
+        private static final String SUFFIX = ".compressed";
+
+        public Compressor() {
+        }
+
+        public String getHelp() {
+            return "compress <filename>: Compresses a file using gzip compression";
+        }
+
+        public int getArity() {
+            return 1;
+        }
+
+        public void run() throws IOException {
+            final String inFilename = mArgs[0];
+            final String outFilename = inFilename + SUFFIX;
+            final FileInputStream input = new FileInputStream(new File(inFilename));
+            final FileOutputStream output = new FileOutputStream(new File(outFilename));
+            copy(input, new GZIPOutputStream(output));
+        }
+    }
+
+    static public class Uncompressor extends Dicttool.Command {
+        public static final String COMMAND = "uncompress";
+        private static final String SUFFIX = ".uncompressed";
+
+        public Uncompressor() {
+        }
+
+        public String getHelp() {
+            return "uncompress <filename>: Uncompresses a file compressed with gzip compression";
+        }
+
+        public int getArity() {
+            return 1;
+        }
+
+        public void run() throws IOException {
+            final String inFilename = mArgs[0];
+            final String outFilename = inFilename + SUFFIX;
+            final FileInputStream input = new FileInputStream(new File(inFilename));
+            final FileOutputStream output = new FileOutputStream(new File(outFilename));
+            copy(new GZIPInputStream(input), output);
+        }
+    }
+}
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java
new file mode 100644
index 0000000..b78be79
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java
@@ -0,0 +1,120 @@
+/**
+ * Copyright (C) 2012 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.dicttool;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class Dicttool {
+
+    public static abstract class Command {
+        protected String[] mArgs;
+        public void setArgs(String[] args) throws IllegalArgumentException {
+            mArgs = args;
+        }
+        abstract public int getArity();
+        abstract public String getHelp();
+        abstract public void run() throws Exception;
+    }
+    static HashMap<String, Class<? extends Command>> sCommands =
+            new HashMap<String, Class<? extends Command>>();
+    static {
+        sCommands.put("info", Info.class);
+        sCommands.put("compress", Compress.Compressor.class);
+        sCommands.put("uncompress", Compress.Uncompressor.class);
+    }
+
+    private static Command getCommandInstance(final String commandName) {
+        try {
+            return sCommands.get(commandName).newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(commandName + " is not installed");
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(commandName + " is not installed");
+        }
+    }
+
+    private static void help() {
+        System.out.println("Syntax: dicttool <command [arguments]>\nAvailable commands:\n");
+        for (final String commandName : sCommands.keySet()) {
+            System.out.println("*** " + commandName);
+            System.out.println(getCommandInstance(commandName).getHelp());
+            System.out.println("");
+        }
+    }
+
+    private static boolean isCommand(final String commandName) {
+        return sCommands.containsKey(commandName);
+    }
+
+    private String mPreviousCommand = null; // local to the getNextCommand function
+    private Command getNextCommand(final ArrayList<String> arguments) {
+        final String firstArgument = arguments.get(0);
+        final String commandName;
+        if (isCommand(firstArgument)) {
+            commandName = firstArgument;
+            arguments.remove(0);
+        } else if (isCommand(mPreviousCommand)) {
+            commandName = mPreviousCommand;
+        } else {
+            throw new RuntimeException("Unknown command : " + firstArgument);
+        }
+        final Command command = getCommandInstance(commandName);
+        final int arity = command.getArity();
+        if (arguments.size() < arity) {
+            throw new RuntimeException("Not enough arguments to command " + commandName);
+        }
+        final String[] argsArray = new String[arity];
+        arguments.subList(0, arity).toArray(argsArray);
+        for (int i = 0; i < arity; ++i) {
+            // For some reason, ArrayList#removeRange is protected
+            arguments.remove(0);
+        }
+        command.setArgs(argsArray);
+        mPreviousCommand = commandName;
+        return command;
+    }
+
+    private void execute(final ArrayList<String> arguments) {
+        ArrayList<Command> commandsToExecute = new ArrayList<Command>();
+        while (!arguments.isEmpty()) {
+            commandsToExecute.add(getNextCommand(arguments));
+        }
+        for (final Command command : commandsToExecute) {
+            try {
+                command.run();
+            } catch (Exception e) {
+                System.out.println("Exception while processing command "
+                        + command.getClass().getSimpleName() + " : " + e);
+                return;
+            }
+        }
+    }
+
+    public static void main(final String[] args) {
+        if (0 == args.length) {
+            help();
+            return;
+        }
+        if (!isCommand(args[0])) throw new RuntimeException("Unknown command : " + args[0]);
+
+        final ArrayList<String> arguments = new ArrayList<String>(args.length);
+        arguments.addAll(Arrays.asList(args));
+        new Dicttool().execute(arguments);
+    }
+}
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Info.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Info.java
new file mode 100644
index 0000000..cb032dd
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Info.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2012 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.dicttool;
+
+public class Info extends Dicttool.Command {
+    public Info() {
+    }
+
+    public String getHelp() {
+        return "info <filename>: prints various information about a dictionary file";
+    }
+
+    public int getArity() {
+        return 1;
+    }
+
+    public void run() {
+        // TODO: implement this
+        System.out.println("Not implemented yet");
+    }
+}
diff --git a/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java b/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java
index 366d73e..07a6c30 100644
--- a/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java
+++ b/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java
@@ -27,14 +27,13 @@
 import java.util.jar.JarFile;
 
 public class JarUtils {
-    private static final String MANIFEST = "META-INF/MANIFEST.MF";
-
     private JarUtils() {
         // This utility class is not publicly instantiable.
     }
 
-    public static JarFile getJarFile(final ClassLoader loader) {
-        final URL resUrl = loader.getResource(MANIFEST);
+    public static JarFile getJarFile(final Class<?> mainClass) {
+        final String mainClassPath = "/" + mainClass.getName().replace('.', '/') + ".class";
+        final URL resUrl = mainClass.getResource(mainClassPath);
         if (!resUrl.getProtocol().equals("jar")) {
             throw new RuntimeException("Should run as jar");
         }
diff --git a/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java b/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java
index a5abcf1..4a92369 100644
--- a/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java
+++ b/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java
@@ -58,7 +58,7 @@
 
     public static void main(final String[] args) {
         final Options options = new Options(args);
-        final JarFile jar = JarUtils.getJarFile(LabelText.class.getClassLoader());
+        final JarFile jar = JarUtils.getJarFile(LabelText.class);
         final MoreKeysResources resources = new MoreKeysResources(jar);
         resources.writeToJava(options.mJava);
     }
diff --git a/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java b/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java
index a483593..37ac0d0 100644
--- a/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java
+++ b/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java
@@ -100,7 +100,7 @@
                 final File outputFile = new File(outPackage,
                         JAVA_TEMPLATE.replace(".tmpl", ".java"));
                 outPackage.mkdirs();
-                ps = new PrintStream(outputFile);
+                ps = new PrintStream(outputFile, "UTF-8");
             }
             lnr = new LineNumberReader(new InputStreamReader(JarUtils.openResource(template)));
             inflateTemplate(lnr, ps);