Merge remote-tracking branch 'goog/jb-mr1-dev' into mergescriptpackage
diff --git a/dictionaries/en_whitelist.xml b/dictionaries/en_whitelist.xml
new file mode 100644
index 0000000..e11935f
--- /dev/null
+++ b/dictionaries/en_whitelist.xml
@@ -0,0 +1,297 @@
+<?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.
+*/
+-->
+<shortcuts>
+ <entry shortcut="ill">
+  <target replacement="I'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="acomodate">
+  <target replacement="accommodate" priority="whitelist" />
+ </entry>
+ <entry shortcut="aint">
+  <target replacement="ain't" priority="whitelist" />
+ </entry>
+ <entry shortcut="alot">
+  <target replacement="a lot" priority="whitelist" />
+ </entry>
+ <entry shortcut="andteh">
+  <target replacement="and the" priority="whitelist" />
+ </entry>
+ <entry shortcut="arent">
+  <target replacement="aren't" priority="whitelist" />
+ </entry>
+ <entry shortcut="bern">
+  <target replacement="been" priority="whitelist" />
+ </entry>
+ <entry shortcut="bot">
+  <target replacement="not" priority="whitelist" />
+ </entry>
+ <entry shortcut="bur">
+  <target replacement="but" priority="whitelist" />
+ </entry>
+ <entry shortcut="cam">
+  <target replacement="can" priority="whitelist" />
+ </entry>
+ <entry shortcut="cant">
+  <target replacement="can't" priority="whitelist" />
+ </entry>
+ <entry shortcut="dame">
+  <target replacement="same" priority="whitelist" />
+ </entry>
+ <entry shortcut="didint">
+  <target replacement="didn't" priority="whitelist" />
+ </entry>
+ <entry shortcut="dormer">
+  <target replacement="former" priority="whitelist" />
+ </entry>
+ <entry shortcut="dud">
+  <target replacement="did" priority="whitelist" />
+ </entry>
+ <entry shortcut="fay">
+  <target replacement="day" priority="whitelist" />
+ </entry>
+ <entry shortcut="fife">
+  <target replacement="five" priority="whitelist" />
+ </entry>
+ <entry shortcut="foo">
+  <target replacement="for" priority="whitelist" />
+ </entry>
+ <entry shortcut="fora">
+  <target replacement="for a" priority="whitelist" />
+ </entry>
+ <entry shortcut="galled">
+  <target replacement="called" priority="whitelist" />
+ </entry>
+ <entry shortcut="goo">
+  <target replacement="too" priority="whitelist" />
+ </entry>
+ <entry shortcut="hed">
+  <target replacement="he'd" priority="whitelist" />
+ </entry>
+ <entry shortcut="hel">
+  <target replacement="he'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="heres">
+  <target replacement="here's" priority="whitelist" />
+ </entry>
+ <entry shortcut="hew">
+  <target replacement="new" priority="whitelist" />
+ </entry>
+ <entry shortcut="hoe">
+  <target replacement="how" priority="whitelist" />
+ </entry>
+ <entry shortcut="hoes">
+  <target replacement="how's" priority="whitelist" />
+ </entry>
+ <entry shortcut="howd">
+  <target replacement="how'd" priority="whitelist" />
+ </entry>
+ <entry shortcut="howll">
+  <target replacement="how'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="hows">
+  <target replacement="how's" priority="whitelist" />
+ </entry>
+ <entry shortcut="howve">
+  <target replacement="how've" priority="whitelist" />
+ </entry>
+ <entry shortcut="hum">
+  <target replacement="him" priority="whitelist" />
+ </entry>
+ <entry shortcut="i">
+  <target replacement="I" priority="whitelist" />
+ </entry>
+ <entry shortcut="ifs">
+  <target replacement="its" priority="whitelist" />
+ </entry>
+ <entry shortcut="il">
+  <target replacement="I'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="im">
+  <target replacement="I'm" priority="whitelist" />
+ </entry>
+ <entry shortcut="inteh">
+  <target replacement="in the" priority="whitelist" />
+ </entry>
+ <entry shortcut="itd">
+  <target replacement="it'd" priority="whitelist" />
+ </entry>
+ <entry shortcut="itsa">
+  <target replacement="it's a" priority="whitelist" />
+ </entry>
+ <entry shortcut="lets">
+  <target replacement="let's" priority="whitelist" />
+ </entry>
+ <entry shortcut="maam">
+  <target replacement="ma'am" priority="whitelist" />
+ </entry>
+ <entry shortcut="manu">
+  <target replacement="many" priority="whitelist" />
+ </entry>
+ <entry shortcut="mare">
+  <target replacement="made" priority="whitelist" />
+ </entry>
+ <entry shortcut="mew">
+  <target replacement="new" priority="whitelist" />
+ </entry>
+ <entry shortcut="mire">
+  <target replacement="more" priority="whitelist" />
+ </entry>
+ <entry shortcut="moat">
+  <target replacement="most" priority="whitelist" />
+ </entry>
+ <entry shortcut="mot">
+  <target replacement="not" priority="whitelist" />
+ </entry>
+ <entry shortcut="mote">
+  <target replacement="note" priority="whitelist" />
+ </entry>
+ <entry shortcut="motes">
+  <target replacement="notes" priority="whitelist" />
+ </entry>
+ <entry shortcut="mow">
+  <target replacement="now" priority="whitelist" />
+ </entry>
+ <entry shortcut="namer">
+  <target replacement="named" priority="whitelist" />
+ </entry>
+ <entry shortcut="nave">
+  <target replacement="have" priority="whitelist" />
+ </entry>
+ <entry shortcut="nee">
+  <target replacement="new" priority="whitelist" />
+ </entry>
+ <entry shortcut="nigh">
+  <target replacement="high" priority="whitelist" />
+ </entry>
+ <entry shortcut="nit">
+  <target replacement="not" priority="whitelist" />
+ </entry>
+ <entry shortcut="oft">
+  <target replacement="off" priority="whitelist" />
+ </entry>
+ <entry shortcut="os">
+  <target replacement="is" priority="whitelist" />
+ </entry>
+ <entry shortcut="pater">
+  <target replacement="later" priority="whitelist" />
+ </entry>
+ <entry shortcut="rook">
+  <target replacement="took" priority="whitelist" />
+ </entry>
+ <entry shortcut="shel">
+  <target replacement="she'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="shouldent">
+  <target replacement="shouldn't" priority="whitelist" />
+ </entry>
+ <entry shortcut="sill">
+  <target replacement="will" priority="whitelist" />
+ </entry>
+ <entry shortcut="sown">
+  <target replacement="down" priority="whitelist" />
+ </entry>
+ <entry shortcut="thatd">
+  <target replacement="that'd" priority="whitelist" />
+ </entry>
+ <entry shortcut="tine">
+  <target replacement="time" priority="whitelist" />
+ </entry>
+ <entry shortcut="thong">
+  <target replacement="thing" priority="whitelist" />
+ </entry>
+ <entry shortcut="tome">
+  <target replacement="time" priority="whitelist" />
+ </entry>
+ <entry shortcut="uf">
+  <target replacement="if" priority="whitelist" />
+ </entry>
+ <entry shortcut="un">
+  <target replacement="in" priority="whitelist" />
+ </entry>
+ <entry shortcut="UnitedStates">
+  <target replacement="United States" priority="whitelist" />
+ </entry>
+ <entry shortcut="unitedstates">
+  <target replacement="United States" priority="whitelist" />
+ </entry>
+ <entry shortcut="visavis">
+  <target replacement="vis-a-vis" priority="whitelist" />
+ </entry>
+ <entry shortcut="wierd">
+  <target replacement="weird" priority="whitelist" />
+ </entry>
+ <entry shortcut="wel">
+  <target replacement="we'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="wer">
+  <target replacement="we're" priority="whitelist" />
+ </entry>
+ <entry shortcut="whatd">
+  <target replacement="what'd" priority="whitelist" />
+ </entry>
+ <entry shortcut="whatm">
+  <target replacement="what'm" priority="whitelist" />
+ </entry>
+ <entry shortcut="whatre">
+  <target replacement="what're" priority="whitelist" />
+ </entry>
+ <entry shortcut="whats">
+  <target replacement="what's" priority="whitelist" />
+ </entry>
+ <entry shortcut="whens">
+  <target replacement="when's" priority="whitelist" />
+ </entry>
+ <entry shortcut="whered">
+  <target replacement="where'd" priority="whitelist" />
+ </entry>
+ <entry shortcut="wherell">
+  <target replacement="where'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="wheres">
+  <target replacement="where's" priority="whitelist" />
+ </entry>
+ <entry shortcut="wholl">
+  <target replacement="who'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="whove">
+  <target replacement="who've" priority="whitelist" />
+ </entry>
+ <entry shortcut="whyd">
+  <target replacement="why'd" priority="whitelist" />
+ </entry>
+ <entry shortcut="whyll">
+  <target replacement="why'll" priority="whitelist" />
+ </entry>
+ <entry shortcut="whys">
+  <target replacement="why's" priority="whitelist" />
+ </entry>
+ <entry shortcut="whyve">
+  <target replacement="why've" priority="whitelist" />
+ </entry>
+ <entry shortcut="wont">
+  <target replacement="won't" priority="whitelist" />
+ </entry>
+ <entry shortcut="yall">
+  <target replacement="y'all" priority="whitelist" />
+ </entry>
+ <entry shortcut="youd">
+  <target replacement="you'd" priority="whitelist" />
+ </entry>
+</shortcuts>
diff --git a/java/proguard.flags b/java/proguard.flags
index 24b4c19..ac5b7df 100644
--- a/java/proguard.flags
+++ b/java/proguard.flags
@@ -44,6 +44,10 @@
   <init>(...);
 }
 
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
 -keep class com.android.inputmethod.research.ResearchLogger {
   void flush();
   void publishCurrentLogUnit(...);
diff --git a/java/res/values-en/whitelist.xml b/java/res/values-en/whitelist.xml
deleted file mode 100644
index 2620179..0000000
--- a/java/res/values-en/whitelist.xml
+++ /dev/null
@@ -1,411 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2011, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!--
-        An entry of the whitelist word should be:
-        1. (int)frequency
-        2. (String)before
-        3. (String)after
-     -->
-    <string-array name="wordlist_whitelist" translatable="false">
-
-        <item>255</item>
-        <item>ill</item>
-        <item>I\'ll</item>
-
-        <!-- TODO: Trim down more entries by removing ones that get auto-corrected by the
-             Android keyboard's own typing error correction algorithms. -->
-
-        <item>255</item>
-        <item>acomodate</item>
-        <item>accommodate</item>
-
-        <item>255</item>
-        <item>aint</item>
-        <item>ain\'t</item>
-
-        <item>255</item>
-        <item>alot</item>
-        <item>a lot</item>
-
-        <item>255</item>
-        <item>andteh</item>
-        <item>and the</item>
-
-        <item>255</item>
-        <item>arent</item>
-        <item>aren\'t</item>
-
-        <item>255</item>
-        <item>bot</item>
-        <item>not</item>
-
-        <item>255</item>
-        <item>bern</item>
-        <item>been</item>
-
-        <item>255</item>
-        <item>bot</item>
-        <item>not</item>
-
-        <item>255</item>
-        <item>bur</item>
-        <item>but</item>
-
-        <item>255</item>
-        <item>cam</item>
-        <item>can</item>
-
-        <item>255</item>
-        <item>cant</item>
-        <item>can\'t</item>
-
-        <item>255</item>
-        <item>dame</item>
-        <item>same</item>
-
-        <item>255</item>
-        <item>didint</item>
-        <item>didn\'t</item>
-
-        <item>255</item>
-        <item>dormer</item>
-        <item>former</item>
-
-        <item>255</item>
-        <item>dud</item>
-        <item>did</item>
-
-        <item>255</item>
-        <item>fay</item>
-        <item>day</item>
-
-        <item>255</item>
-        <item>fife</item>
-        <item>five</item>
-
-        <item>255</item>
-        <item>foo</item>
-        <item>for</item>
-
-        <item>255</item>
-        <item>fora</item>
-        <item>for a</item>
-
-        <item>255</item>
-        <item>galled</item>
-        <item>called</item>
-
-        <item>255</item>
-        <item>goo</item>
-        <item>too</item>
-
-        <item>255</item>
-        <item>hed</item>
-        <item>he\'d</item>
-
-        <item>255</item>
-        <item>hel</item>
-        <item>he\'ll</item>
-
-        <item>255</item>
-        <item>heres</item>
-        <item>here\'s</item>
-
-        <item>255</item>
-        <item>hew</item>
-        <item>new</item>
-
-        <item>255</item>
-        <item>hoe</item>
-        <item>how</item>
-
-        <item>255</item>
-        <item>hoes</item>
-        <item>how\'s</item>
-
-        <item>255</item>
-        <item>howd</item>
-        <item>how\'d</item>
-
-        <item>255</item>
-        <item>howll</item>
-        <item>how\'ll</item>
-
-        <item>255</item>
-        <item>hows</item>
-        <item>how\'s</item>
-
-        <item>255</item>
-        <item>howve</item>
-        <item>how\'ve</item>
-
-        <item>255</item>
-        <item>hum</item>
-        <item>him</item>
-
-        <item>255</item>
-        <item>i</item>
-        <item>I</item>
-
-        <item>255</item>
-        <item>ifs</item>
-        <item>its</item>
-
-        <item>255</item>
-        <item>il</item>
-        <item>I\'ll</item>
-
-        <item>255</item>
-        <item>im</item>
-        <item>I\'m</item>
-
-        <item>255</item>
-        <item>inteh</item>
-        <item>in the</item>
-
-        <item>255</item>
-        <item>itd</item>
-        <item>it\'d</item>
-
-        <item>255</item>
-        <item>itsa</item>
-        <item>it\'s a</item>
-
-        <item>255</item>
-        <item>lets</item>
-        <item>let\'s</item>
-
-        <item>255</item>
-        <item>maam</item>
-        <item>ma\'am</item>
-
-        <item>255</item>
-        <item>manu</item>
-        <item>many</item>
-
-        <item>255</item>
-        <item>mare</item>
-        <item>made</item>
-
-        <item>255</item>
-        <item>mew</item>
-        <item>new</item>
-
-        <item>255</item>
-        <item>mire</item>
-        <item>more</item>
-
-        <item>255</item>
-        <item>moat</item>
-        <item>most</item>
-
-        <item>255</item>
-        <item>mot</item>
-        <item>not</item>
-
-        <item>255</item>
-        <item>mote</item>
-        <item>note</item>
-
-        <item>255</item>
-        <item>motes</item>
-        <item>notes</item>
-
-        <item>255</item>
-        <item>mow</item>
-        <item>now</item>
-
-        <item>255</item>
-        <item>namer</item>
-        <item>named</item>
-
-        <item>255</item>
-        <item>nave</item>
-        <item>have</item>
-
-        <item>255</item>
-        <item>nee</item>
-        <item>new</item>
-
-        <item>255</item>
-        <item>nigh</item>
-        <item>high</item>
-
-        <item>255</item>
-        <item>nit</item>
-        <item>not</item>
-
-        <item>255</item>
-        <item>oft</item>
-        <item>off</item>
-
-        <item>255</item>
-        <item>os</item>
-        <item>is</item>
-
-        <item>255</item>
-        <item>pater</item>
-        <item>later</item>
-
-        <item>255</item>
-        <item>rook</item>
-        <item>took</item>
-
-        <item>255</item>
-        <item>shel</item>
-        <item>she\'ll</item>
-
-        <item>255</item>
-        <item>shouldent</item>
-        <item>shouldn\'t</item>
-
-        <item>255</item>
-        <item>sill</item>
-        <item>will</item>
-
-        <item>255</item>
-        <item>sown</item>
-        <item>down</item>
-
-        <item>255</item>
-        <item>thatd</item>
-        <item>that\'d</item>
-
-        <item>255</item>
-        <item>tine</item>
-        <item>time</item>
-
-        <item>255</item>
-        <item>thong</item>
-        <item>thing</item>
-
-        <item>255</item>
-        <item>tome</item>
-        <item>time</item>
-
-        <!-- through additional proximity, 'uf' becomes 'of'. 'o' is not next to 'u' so anyone
-             typing 'uf' probably meant 'if', but 'of' is much more common and should be left
-             higher than 'if', hence the need for this entry. -->
-        <item>255</item>
-        <item>uf</item>
-        <item>if</item>
-
-        <!-- 'un' becomes 'UN' because of perfect match ; even if we remove 'UN', then 'un'
-             will become 'on' for the same reason as above. So list this here. -->
-        <item>255</item>
-        <item>un</item>
-        <item>in</item>
-
-        <!-- does it really make any sense to have the following here? -->
-        <item>255</item>
-        <item>UnitedStates</item>
-        <item>United States</item>
-
-        <item>255</item>
-        <item>unitedstates</item>
-        <item>United States</item>
-
-        <item>255</item>
-        <item>visavis</item>
-        <item>vis-a-vis</item>
-
-        <item>255</item>
-        <item>wierd</item>
-        <item>weird</item>
-
-        <item>255</item>
-        <item>wel</item>
-        <item>we\'ll</item>
-
-        <item>255</item>
-        <item>wer</item>
-        <item>we\'re</item>
-
-        <item>255</item>
-        <item>whatd</item>
-        <item>what\'d</item>
-
-        <item>255</item>
-        <item>whatm</item>
-        <item>what\'m</item>
-
-        <item>255</item>
-        <item>whatre</item>
-        <item>what\'re</item>
-
-        <item>255</item>
-        <item>whats</item>
-        <item>what\'s</item>
-
-        <item>255</item>
-        <item>whens</item>
-        <item>when\'s</item>
-
-        <item>255</item>
-        <item>whered</item>
-        <item>where\'d</item>
-
-        <item>255</item>
-        <item>wherell</item>
-        <item>where\'ll</item>
-
-        <item>255</item>
-        <item>wheres</item>
-        <item>where\'s</item>
-
-        <item>255</item>
-        <item>wholl</item>
-        <item>who\'ll</item>
-
-        <item>255</item>
-        <item>whove</item>
-        <item>who\'ve</item>
-
-        <item>255</item>
-        <item>whyd</item>
-        <item>why\'d</item>
-
-        <item>255</item>
-        <item>whyll</item>
-        <item>why\'ll</item>
-
-        <item>255</item>
-        <item>whys</item>
-        <item>why\'s</item>
-
-        <item>255</item>
-        <item>whyve</item>
-        <item>why\'ve</item>
-
-        <item>255</item>
-        <item>wont</item>
-        <item>won\'t</item>
-
-        <item>255</item>
-        <item>yall</item>
-        <item>y\'all</item>
-
-        <item>255</item>
-        <item>youd</item>
-        <item>you\'d</item>
-
-    </string-array>
-</resources>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 4975d65..76e76cc 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -41,26 +41,24 @@
              checkable+checked+pressed. -->
         <attr name="keyBackground" format="reference" />
 
-        <!-- Size of the text for one letter keys. If not defined, keyLetterRatio takes effect. -->
-        <attr name="keyLetterSize" format="dimension" />
-        <!-- Size of the text for keys with multiple letters. If not defined, keyLabelRatio takes
-             effect. -->
-        <attr name="keyLabelSize" format="dimension" />
-        <!-- Size of the text for one letter keys, in the proportion of key height. -->
-        <attr name="keyLetterRatio" format="float" />
+        <!-- Size of the text for one letter keys. If specified as fraction, the text size is
+             measured in the proportion of key height. -->
+        <attr name="keyLetterSize" format="dimension|fraction" />
+        <!-- Size of the text for keys with multiple letters. If specified as fraction, the text
+             size is measured in the proportion of key height. -->
+        <attr name="keyLabelSize" format="dimension|fraction" />
         <!-- Large size of the text for one letter keys, in the proportion of key height. -->
-        <attr name="keyLargeLetterRatio" format="float" />
-        <!-- Size of the text for keys with multiple letters, in the proportion of key height. -->
-        <attr name="keyLabelRatio" format="float" />
+        <attr name="keyLargeLetterRatio" format="fraction" />
         <!-- Large size of the text for keys with multiple letters, in the proportion of key height. -->
-        <attr name="keyLargeLabelRatio" format="float" />
+        <attr name="keyLargeLabelRatio" format="fraction" />
         <!-- Size of the text for hint letter (= one character hint label), in the proportion of
              key height. -->
-        <attr name="keyHintLetterRatio" format="float" />
+        <attr name="keyHintLetterRatio" format="fraction" />
         <!-- Size of the text for hint label, in the proportion of key height. -->
-        <attr name="keyHintLabelRatio" format="float" />
+        <attr name="keyHintLabelRatio" format="fraction" />
         <!-- Size of the text for shifted letter hint, in the proportion of key height. -->
-        <attr name="keyShiftedLetterHintRatio" format="float" />
+        <attr name="keyShiftedLetterHintRatio" format="dimension|fraction" />
+
         <!-- Horizontal padding of left/right aligned key label to the edge of the key. -->
         <attr name="keyLabelHorizontalPadding" format="dimension" />
         <!-- Right padding of hint letter to the edge of the key.-->
@@ -96,8 +94,8 @@
         <attr name="keyPreviewOffset" format="dimension" />
         <!-- Height of the key press feedback popup. -->
         <attr name="keyPreviewHeight" format="dimension" />
-        <!-- Size of the text for key press feedback popup, int the proportion of key height -->
-        <attr name="keyPreviewTextRatio" format="float" />
+        <!-- Size of the text for key press feedback popup, in the proportion of key height. -->
+        <attr name="keyPreviewTextRatio" format="fraction" />
         <!-- Delay after key releasing and key press feedback dismissing in millisecond -->
         <attr name="keyPreviewLingerTimeout" format="integer" />
 
@@ -131,6 +129,12 @@
         <attr name="gestureFloatingPreviewTextConnectorWidth" format="dimension" />
         <!-- Delay after gesture input and gesture floating preview text dismissing in millisecond -->
         <attr name="gestureFloatingPreviewTextLingerTimeout" format="integer" />
+        <!-- Delay after gesture trail starts fading out in millisecond. -->
+        <attr name="gesturePreviewTrailFadeoutStartDelay" format="integer" />
+        <!-- Duration while gesture preview trail is fading out in millisecond. -->
+        <attr name="gesturePreviewTrailFadeoutDuration" format="integer" />
+        <!-- Interval of updating gesture preview trail in millisecond. -->
+        <attr name="gesturePreviewTrailUpdateInterval" format="integer" />
         <attr name="gesturePreviewTrailColor" format="color" />
         <attr name="gesturePreviewTrailWidth" format="dimension" />
     </declare-styleable>
@@ -181,13 +185,13 @@
         <attr name="colorTypedWord" format="color" />
         <attr name="colorAutoCorrect" format="color" />
         <attr name="colorSuggested" format="color" />
-        <attr name="alphaValidTypedWord" format="integer" />
-        <attr name="alphaTypedWord" format="integer" />
-        <attr name="alphaAutoCorrect" format="integer" />
-        <attr name="alphaSuggested" format="integer" />
-        <attr name="alphaObsoleted" format="integer" />
+        <attr name="alphaValidTypedWord" format="fraction" />
+        <attr name="alphaTypedWord" format="fraction" />
+        <attr name="alphaAutoCorrect" format="fraction" />
+        <attr name="alphaSuggested" format="fraction" />
+        <attr name="alphaObsoleted" format="fraction" />
         <attr name="suggestionsCountInStrip" format="integer" />
-        <attr name="centerSuggestionPercentile" format="integer" />
+        <attr name="centerSuggestionPercentile" format="fraction" />
         <attr name="maxMoreSuggestionsRow" format="integer" />
         <attr name="minMoreSuggestionsWidth" format="float" />
     </declare-styleable>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 54a6687..8e2d43e 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -50,6 +50,9 @@
     -->
     <integer name="config_key_preview_linger_timeout">70</integer>
     <integer name="config_gesture_floating_preview_text_linger_timeout">200</integer>
+    <integer name="config_gesture_preview_trail_fadeout_start_delay">100</integer>
+    <integer name="config_gesture_preview_trail_fadeout_duration">800</integer>
+    <integer name="config_gesture_preview_trail_update_interval">20</integer>
     <!--
          Configuration for MainKeyboardView
     -->
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index c59bad3..4fd942b 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -92,7 +92,7 @@
     <dimen name="suggestion_text_size">18dp</dimen>
     <dimen name="more_suggestions_hint_text_size">27dp</dimen>
     <integer name="suggestions_count_in_strip">3</integer>
-    <integer name="center_suggestion_percentile">36</integer>
+    <fraction name="center_suggestion_percentile">36%</fraction>
 
     <!-- Gesture preview parameters -->
     <dimen name="gesture_preview_trail_width">2.5dp</dimen>
@@ -101,4 +101,7 @@
     <dimen name="gesture_floating_preview_text_shadow_border">17.5dp</dimen>
     <dimen name="gesture_floating_preview_text_shading_border">7.5dp</dimen>
     <dimen name="gesture_floating_preview_text_connector_width">1.0dp</dimen>
+
+    <!-- Inset used in Accessibility mode to avoid accidental key presses when a finger slides off the screen. -->
+    <dimen name="accessibility_edge_slop">8dp</dimen>
 </resources>
diff --git a/java/res/values/keypress-vibration-durations.xml b/java/res/values/keypress-vibration-durations.xml
index 2569f23..8f6b647 100644
--- a/java/res/values/keypress-vibration-durations.xml
+++ b/java/res/values/keypress-vibration-durations.xml
@@ -22,5 +22,6 @@
         <!-- Build.HARDWARE,duration_in_milliseconds -->
         <item>herring,5</item>
         <item>tuna,5</item>
+        <item>mako,20</item>
     </string-array>
 </resources>
diff --git a/java/res/values/keypress-volumes.xml b/java/res/values/keypress-volumes.xml
index 3b433e4..dd86d6c 100644
--- a/java/res/values/keypress-volumes.xml
+++ b/java/res/values/keypress-volumes.xml
@@ -24,5 +24,6 @@
         <item>tuna,0.5</item>
         <item>stingray,0.4</item>
         <item>grouper,0.3</item>
+        <item>mako,0.3</item>
     </string-array>
 </resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 07b3f31..35cbcf3 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -261,7 +261,8 @@
     <string name="research_feedback_dialog_title" translatable="false">Send feedback</string>
     <!-- Text for checkbox option to include user data in feedback for research purposes [CHAR LIMIT=50] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
-    <string name="research_feedback_include_history_label" translatable="false">Include last 5 words entered</string>
+    <!-- TODO: handle multilingual plurals -->
+    <string name="research_feedback_include_history_label" translatable="false">Include last <xliff:g id="word">%d</xliff:g> words entered</string>
     <!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
@@ -288,6 +289,10 @@
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_send_usage_info" translatable="false">Send usage info</string>
 
+    <!-- Name for the research uploading service to be displayed to users.  [CHAR LIMIT=50] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_log_uploader_name" translatable="false">Research Uploader Service</string>
+
     <!-- Preference for input language selection -->
     <string name="select_language">Input languages</string>
 
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index ae67c43..0220c83 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -35,9 +35,9 @@
     <style name="KeyboardView">
         <item name="android:background">@drawable/keyboard_background</item>
         <item name="keyBackground">@drawable/btn_keyboard_key</item>
-        <item name="keyLetterRatio">@fraction/key_letter_ratio</item>
+        <item name="keyLetterSize">@fraction/key_letter_ratio</item>
         <item name="keyLargeLetterRatio">@fraction/key_large_letter_ratio</item>
-        <item name="keyLabelRatio">@fraction/key_label_ratio</item>
+        <item name="keyLabelSize">@fraction/key_label_ratio</item>
         <item name="keyLargeLabelRatio">@fraction/key_large_label_ratio</item>
         <item name="keyHintLetterRatio">@fraction/key_hint_letter_ratio</item>
         <item name="keyHintLabelRatio">@fraction/key_hint_label_ratio</item>
@@ -78,6 +78,9 @@
         <item name="gestureFloatingPreviewTextConnectorColor">@android:color/white</item>
         <item name="gestureFloatingPreviewTextConnectorWidth">@dimen/gesture_floating_preview_text_connector_width</item>
         <item name="gestureFloatingPreviewTextLingerTimeout">@integer/config_gesture_floating_preview_text_linger_timeout</item>
+        <item name="gesturePreviewTrailFadeoutStartDelay">@integer/config_gesture_preview_trail_fadeout_start_delay</item>
+        <item name="gesturePreviewTrailFadeoutDuration">@integer/config_gesture_preview_trail_fadeout_duration</item>
+        <item name="gesturePreviewTrailUpdateInterval">@integer/config_gesture_preview_trail_update_interval</item>
         <item name="gesturePreviewTrailColor">@android:color/holo_blue_light</item>
         <item name="gesturePreviewTrailWidth">@dimen/gesture_preview_trail_width</item>
         <!-- Common attributes of MainKeyboardView -->
@@ -135,9 +138,9 @@
         <item name="colorTypedWord">@android:color/white</item>
         <item name="colorAutoCorrect">#FFFCAE00</item>
         <item name="colorSuggested">#FFFCAE00</item>
-        <item name="alphaObsoleted">50</item>
+        <item name="alphaObsoleted">50%</item>
         <item name="suggestionsCountInStrip">@integer/suggestions_count_in_strip</item>
-        <item name="centerSuggestionPercentile">@integer/center_suggestion_percentile</item>
+        <item name="centerSuggestionPercentile">@fraction/center_suggestion_percentile</item>
         <item name="maxMoreSuggestionsRow">@integer/max_more_suggestions_row</item>
         <item name="minMoreSuggestionsWidth">@fraction/min_more_suggestions_width</item>
     </style>
@@ -370,12 +373,12 @@
         <item name="colorTypedWord">@android:color/holo_blue_light</item>
         <item name="colorAutoCorrect">@android:color/holo_blue_light</item>
         <item name="colorSuggested">@android:color/holo_blue_light</item>
-        <item name="alphaValidTypedWord">85</item>
-        <item name="alphaTypedWord">85</item>
-        <item name="alphaSuggested">70</item>
-        <item name="alphaObsoleted">70</item>
+        <item name="alphaValidTypedWord">85%</item>
+        <item name="alphaTypedWord">85%</item>
+        <item name="alphaSuggested">70%</item>
+        <item name="alphaObsoleted">70%</item>
         <item name="suggestionsCountInStrip">@integer/suggestions_count_in_strip</item>
-        <item name="centerSuggestionPercentile">@integer/center_suggestion_percentile</item>
+        <item name="centerSuggestionPercentile">@fraction/center_suggestion_percentile</item>
         <item name="maxMoreSuggestionsRow">@integer/max_more_suggestions_row</item>
         <item name="minMoreSuggestionsWidth">@fraction/min_more_suggestions_width</item>
     </style>
diff --git a/java/res/values/whitelist.xml b/java/res/values/whitelist.xml
deleted file mode 100644
index d4ecbfa..0000000
--- a/java/res/values/whitelist.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2011, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!--
-        An entry of the whitelist word should be:
-        1. (int)frequency
-        2. (String)before
-        3. (String)after
-     -->
-    <string-array name="wordlist_whitelist">
-    </string-array>
-</resources>
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
index 70e38fd..039c77b 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
@@ -35,6 +35,7 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.latin.CollectionUtils;
 
 /**
  * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
@@ -55,7 +56,7 @@
     private final AccessibilityUtils mAccessibilityUtils;
 
     /** A map of integer IDs to {@link Key}s. */
-    private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>();
+    private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray();
 
     /** Temporary rect used to calculate in-screen bounds. */
     private final Rect mTempBoundsInScreen = new Rect();
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
index 616b1c6..58d3022 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -19,10 +19,15 @@
 import android.content.Context;
 import android.inputmethodservice.InputMethodService;
 import android.media.AudioManager;
+import android.os.Build;
 import android.os.SystemClock;
 import android.provider.Settings;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
 import android.util.Log;
 import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.inputmethod.EditorInfo;
@@ -138,9 +143,10 @@
      * Sends the specified text to the {@link AccessibilityManager} to be
      * spoken.
      *
-     * @param text the text to speak
+     * @param view The source view.
+     * @param text The text to speak.
      */
-    public void speak(CharSequence text) {
+    public void announceForAccessibility(View view, CharSequence text) {
         if (!mAccessibilityManager.isEnabled()) {
             Log.e(TAG, "Attempted to speak when accessibility was disabled!");
             return;
@@ -149,8 +155,7 @@
         // The following is a hack to avoid using the heavy-weight TextToSpeech
         // class. Instead, we're just forcing a fake AccessibilityEvent into
         // the screen reader to make it speak.
-        final AccessibilityEvent event = AccessibilityEvent
-                .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        final AccessibilityEvent event = AccessibilityEvent.obtain();
 
         event.setPackageName(PACKAGE);
         event.setClassName(CLASS);
@@ -158,20 +163,34 @@
         event.setEnabled(true);
         event.getText().add(text);
 
-        mAccessibilityManager.sendAccessibilityEvent(event);
+        // Platforms starting at SDK 16 should use announce events.
+        if (Build.VERSION.SDK_INT >= 16) {
+            event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
+        } else {
+            event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        }
+
+        final ViewParent viewParent = view.getParent();
+        if ((viewParent == null) || !(viewParent instanceof ViewGroup)) {
+            Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility");
+            return;
+        }
+
+        viewParent.requestSendAccessibilityEvent(view, event);
     }
 
     /**
      * Handles speaking the "connect a headset to hear passwords" notification
      * when connecting to a password field.
      *
+     * @param view The source view.
      * @param editorInfo The input connection's editor info attribute.
      * @param restarting Whether the connection is being restarted.
      */
-    public void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
+    public void onStartInputViewInternal(View view, EditorInfo editorInfo, boolean restarting) {
         if (shouldObscureInput(editorInfo)) {
             final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
-            speak(text);
+            announceForAccessibility(view, text);
         }
     }
 
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index 4ecbf82..e42de0b 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -24,7 +24,6 @@
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewConfiguration;
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
@@ -44,7 +43,7 @@
 
     /**
      * Inset in pixels to look for keys when the user's finger exits the
-     * keyboard area. See {@link ViewConfiguration#getScaledEdgeSlop()}.
+     * keyboard area.
      */
     private int mEdgeSlop;
 
@@ -62,7 +61,8 @@
 
     private void initInternal(InputMethodService inputMethod) {
         mInputMethod = inputMethod;
-        mEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop();
+        mEdgeSlop = inputMethod.getResources().getDimensionPixelSize(
+                R.dimen.accessibility_edge_slop);
     }
 
     /**
@@ -114,8 +114,14 @@
     public boolean dispatchHoverEvent(MotionEvent event, PointerTracker tracker) {
         final int x = (int) event.getX();
         final int y = (int) event.getY();
-        final Key key = tracker.getKeyOn(x, y);
         final Key previousKey = mLastHoverKey;
+        final Key key;
+
+        if (pointInView(x, y)) {
+            key = tracker.getKeyOn(x, y);
+        } else {
+            key = null;
+        }
 
         mLastHoverKey = key;
 
@@ -123,7 +129,7 @@
         case MotionEvent.ACTION_HOVER_EXIT:
             // Make sure we're not getting an EXIT event because the user slid
             // off the keyboard area, then force a key press.
-            if (pointInView(x, y) && (key != null)) {
+            if (key != null) {
                 getAccessibilityNodeProvider().simulateKeyPress(key);
             }
             //$FALL-THROUGH$
@@ -250,7 +256,7 @@
             text = context.getText(R.string.spoken_description_shiftmode_off);
         }
 
-        AccessibilityUtils.getInstance().speak(text);
+        AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
     }
 
     /**
@@ -290,6 +296,6 @@
         }
 
         final String text = context.getString(resId);
-        AccessibilityUtils.getInstance().speak(text);
+        AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
     }
 }
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 9b74070..5c45448 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -25,6 +25,7 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 
 import java.util.HashMap;
@@ -38,7 +39,7 @@
     private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
 
     // Map of key labels to spoken description resource IDs
-    private final HashMap<CharSequence, Integer> mKeyLabelMap;
+    private final HashMap<CharSequence, Integer> mKeyLabelMap = CollectionUtils.newHashMap();
 
     // Sparse array of spoken description resource IDs indexed by key codes
     private final SparseIntArray mKeyCodeMap;
@@ -52,7 +53,6 @@
     }
 
     private KeyCodeDescriptionMapper() {
-        mKeyLabelMap = new HashMap<CharSequence, Integer>();
         mKeyCodeMap = new SparseIntArray();
     }
 
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
index 1183b5f..6ba309f 100644
--- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
+++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
@@ -16,10 +16,6 @@
 
 package com.android.inputmethod.compat;
 
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver;
-
 import android.content.Context;
 import android.text.Spannable;
 import android.text.SpannableString;
@@ -27,6 +23,11 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver;
+
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
@@ -119,7 +120,7 @@
         } else {
             spannable = new SpannableString(pickedWord);
         }
-        final ArrayList<String> suggestionsList = new ArrayList<String>();
+        final ArrayList<String> suggestionsList = CollectionUtils.newArrayList();
         for (int i = 0; i < suggestedWords.size(); ++i) {
             if (suggestionsList.size() >= OBJ_SUGGESTIONS_MAX_SIZE) {
                 break;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 97d88af..868c8ca 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -16,10 +16,10 @@
 
 package com.android.inputmethod.keyboard;
 
+import com.android.inputmethod.latin.Constants;
+
 
 public class KeyDetector {
-    public static final int NOT_A_CODE = -1;
-
     private final int mKeyHysteresisDistanceSquared;
 
     private Keyboard mKeyboard;
@@ -59,6 +59,9 @@
     }
 
     public Keyboard getKeyboard() {
+        if (mKeyboard == null) {
+            throw new IllegalStateException("keyboard isn't set");
+        }
         return mKeyboard;
     }
 
@@ -100,7 +103,7 @@
         final StringBuilder sb = new StringBuilder();
         boolean addDelimiter = false;
         for (final int code : codes) {
-            if (code == NOT_A_CODE) break;
+            if (code == Constants.NOT_A_CODE) break;
             if (addDelimiter) sb.append(", ");
             sb.append(Keyboard.printableCode(code));
             addDelimiter = true;
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 3abe890..e37868b 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -33,6 +33,7 @@
 import com.android.inputmethod.keyboard.internal.KeyboardCodesSet;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
 import com.android.inputmethod.keyboard.internal.KeyboardTextsSet;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
 import com.android.inputmethod.latin.R;
@@ -134,7 +135,7 @@
     public final Key[] mAltCodeKeysWhileTyping;
     public final KeyboardIconsSet mIconsSet;
 
-    private final SparseArray<Key> mKeyCache = new SparseArray<Key>();
+    private final SparseArray<Key> mKeyCache = CollectionUtils.newSparseArray();
 
     private final ProximityInfo mProximityInfo;
     private final boolean mProximityCharsCorrectionEnabled;
@@ -219,6 +220,11 @@
         return code >= CODE_SPACE;
     }
 
+    @Override
+    public String toString() {
+        return mId.toString();
+    }
+
     public static class Params {
         public KeyboardId mId;
         public int mThemeId;
@@ -249,9 +255,9 @@
         public int GRID_WIDTH;
         public int GRID_HEIGHT;
 
-        public final HashSet<Key> mKeys = new HashSet<Key>();
-        public final ArrayList<Key> mShiftKeys = new ArrayList<Key>();
-        public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<Key>();
+        public final HashSet<Key> mKeys = CollectionUtils.newHashSet();
+        public final ArrayList<Key> mShiftKeys = CollectionUtils.newArrayList();
+        public final ArrayList<Key> mAltCodeKeysWhileTyping = CollectionUtils.newArrayList();
         public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
         public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
         public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
@@ -278,9 +284,10 @@
             public void load(String[] data) {
                 final int dataLength = data.length;
                 if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) {
-                    if (LatinImeLogger.sDBG)
+                    if (LatinImeLogger.sDBG) {
                         throw new RuntimeException(
                                 "the size of touch position correction data is invalid");
+                    }
                     return;
                 }
 
@@ -319,7 +326,7 @@
 
             public boolean isValid() {
                 return mEnabled && mXs != null && mYs != null && mRadii != null
-                    && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0;
+                        && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0;
             }
         }
 
@@ -865,10 +872,12 @@
             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
                     R.styleable.Keyboard);
             try {
-                if (a.hasValue(R.styleable.Keyboard_horizontalGap))
+                if (a.hasValue(R.styleable.Keyboard_horizontalGap)) {
                     throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
-                if (a.hasValue(R.styleable.Keyboard_verticalGap))
+                }
+                if (a.hasValue(R.styleable.Keyboard_verticalGap)) {
                     throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
+                }
                 return new Row(mResources, mParams, parser, mCurrentY);
             } finally {
                 a.recycle();
@@ -916,7 +925,9 @@
                 throws XmlPullParserException, IOException {
             if (skip) {
                 XmlParseUtils.checkEndTag(TAG_KEY, parser);
-                if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
+                if (DEBUG) {
+                    startEndTag("<%s /> skipped", TAG_KEY);
+                }
             } else {
                 final Key key = new Key(mResources, mParams, row, parser);
                 if (DEBUG) {
@@ -1094,9 +1105,9 @@
 
         private boolean parseCaseCondition(XmlPullParser parser) {
             final KeyboardId id = mParams.mId;
-            if (id == null)
+            if (id == null) {
                 return true;
-
+            }
             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
                     R.styleable.Keyboard_Case);
             try {
@@ -1200,9 +1211,9 @@
             // If <case> does not have "index" attribute, that means this <case> is wild-card for
             // the attribute.
             final TypedValue v = a.peekValue(index);
-            if (v == null)
+            if (v == null) {
                 return true;
-
+            }
             if (isIntegerValue(v)) {
                 return intValue == a.getInt(index, 0);
             } else if (isStringValue(v)) {
@@ -1213,8 +1224,9 @@
 
         private static boolean stringArrayContains(String[] array, String value) {
             for (final String elem : array) {
-                if (elem.equals(value))
+                if (elem.equals(value)) {
                     return true;
+                }
             }
             return false;
         }
@@ -1237,16 +1249,18 @@
             TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
                     R.styleable.Keyboard_Key);
             try {
-                if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
+                if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
                     throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
                             + "/> needs styleName attribute", parser);
+                }
                 if (DEBUG) {
                     startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
-                        keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
-                        skip ? " skipped" : "");
+                            keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
+                            skip ? " skipped" : "");
                 }
-                if (!skip)
+                if (!skip) {
                     mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
+                }
             } finally {
                 keyStyleAttr.recycle();
                 keyAttrs.recycle();
@@ -1267,8 +1281,9 @@
         }
 
         private void endRow(Row row) {
-            if (mCurrentRow == null)
+            if (mCurrentRow == null) {
                 throw new InflateException("orphan end row tag");
+            }
             if (mRightEdgeKey != null) {
                 mRightEdgeKey.markAsRightEdge(mParams);
                 mRightEdgeKey = null;
@@ -1304,8 +1319,9 @@
         public static float getDimensionOrFraction(TypedArray a, int index, int base,
                 float defValue) {
             final TypedValue value = a.peekValue(index);
-            if (value == null)
+            if (value == null) {
                 return defValue;
+            }
             if (isFractionValue(value)) {
                 return a.getFraction(index, base, base, defValue);
             } else if (isDimensionValue(value)) {
@@ -1316,8 +1332,9 @@
 
         public static int getEnumValue(TypedArray a, int index, int defValue) {
             final TypedValue value = a.peekValue(index);
-            if (value == null)
+            if (value == null) {
                 return defValue;
+            }
             if (isIntegerValue(value)) {
                 return a.getInt(index, defValue);
             }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index b1621a5..5c8f78f 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.keyboard;
 
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.InputPointers;
 
 public interface KeyboardActionListener {
@@ -44,21 +45,16 @@
      *
      * @param primaryCode this is the code of the key that was pressed
      * @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
-     *            {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}.
-     *            If it's called on insertion from the suggestion strip, it should be
-     *            {@link #SUGGESTION_STRIP_COORDINATE}.
+     *            {@link PointerTracker} or so, the value should be
+     *            {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the
+     *            suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
      * @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
-     *            {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}.
-     *            If it's called on insertion from the suggestion strip, it should be
-     *            {@link #SUGGESTION_STRIP_COORDINATE}.
+     *            {@link PointerTracker} or so, the value should be
+     *            {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the
+     *            suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
      */
     public void onCodeInput(int primaryCode, int x, int y);
 
-    // See {@link Adapter#isInvalidCoordinate(int)}.
-    public static final int NOT_A_TOUCH_COORDINATE = -1;
-    public static final int SUGGESTION_STRIP_COORDINATE = -2;
-    public static final int SPELL_CHECKER_COORDINATE = -3;
-
     /**
      * Sends a sequence of characters to the listener.
      *
@@ -119,9 +115,9 @@
 
         // TODO: Remove this method when the vertical correction is removed.
         public static boolean isInvalidCoordinate(int coordinate) {
-            // Detect {@link KeyboardActionListener#NOT_A_TOUCH_COORDINATE},
-            // {@link KeyboardActionListener#SUGGESTION_STRIP_COORDINATE}, and
-            // {@link KeyboardActionListener#SPELL_CHECKER_COORDINATE}.
+            // Detect {@link Constants#NOT_A_COORDINATE},
+            // {@link Constants#SUGGESTION_STRIP_COORDINATE}, and
+            // {@link Constants#SPELL_CHECKER_COORDINATE}.
             return coordinate < 0;
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index 64b3f09..76ac3de 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -36,6 +36,7 @@
 
 import com.android.inputmethod.compat.EditorInfoCompatUtils;
 import com.android.inputmethod.keyboard.KeyboardLayoutSet.Params.ElementParams;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.InputTypeUtils;
 import com.android.inputmethod.latin.LatinImeLogger;
@@ -71,7 +72,7 @@
     private final Params mParams;
 
     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
-            new HashMap<KeyboardId, SoftReference<Keyboard>>();
+            CollectionUtils.newHashMap();
     private static final KeysCache sKeysCache = new KeysCache();
 
     public static class KeyboardLayoutSetException extends RuntimeException {
@@ -84,11 +85,7 @@
     }
 
     public static class KeysCache {
-        private final HashMap<Key, Key> mMap;
-
-        public KeysCache() {
-            mMap = new HashMap<Key, Key>();
-        }
+        private final HashMap<Key, Key> mMap = CollectionUtils.newHashMap();
 
         public void clear() {
             mMap.clear();
@@ -120,7 +117,7 @@
         int mWidth;
         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
-                new SparseArray<ElementParams>();
+                CollectionUtils.newSparseArray();
 
         static class ElementParams {
             int mKeyboardXmlId;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 10f651a..fd789f0 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -21,7 +21,6 @@
 import android.content.res.Resources;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
-import android.view.InflateException;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.inputmethod.EditorInfo;
@@ -38,7 +37,7 @@
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SettingsValues;
 import com.android.inputmethod.latin.SubtypeSwitcher;
-import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.WordComposer;
 
 public class KeyboardSwitcher implements KeyboardState.SwitchActions {
     private static final String TAG = KeyboardSwitcher.class.getSimpleName();
@@ -46,24 +45,24 @@
     public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916";
 
     static class KeyboardTheme {
-        public final String mName;
         public final int mThemeId;
         public final int mStyleId;
 
-        public KeyboardTheme(String name, int themeId, int styleId) {
-            mName = name;
+        // Note: The themeId should be aligned with "themeId" attribute of Keyboard style
+        // in values/style.xml.
+        public KeyboardTheme(int themeId, int styleId) {
             mThemeId = themeId;
             mStyleId = styleId;
         }
     }
 
     private static final KeyboardTheme[] KEYBOARD_THEMES = {
-        new KeyboardTheme("Basic",            0, R.style.KeyboardTheme),
-        new KeyboardTheme("HighContrast",     1, R.style.KeyboardTheme_HighContrast),
-        new KeyboardTheme("Stone",            6, R.style.KeyboardTheme_Stone),
-        new KeyboardTheme("Stone.Bold",       7, R.style.KeyboardTheme_Stone_Bold),
-        new KeyboardTheme("GingerBread",      8, R.style.KeyboardTheme_Gingerbread),
-        new KeyboardTheme("IceCreamSandwich", 5, R.style.KeyboardTheme_IceCreamSandwich),
+        new KeyboardTheme(0, R.style.KeyboardTheme),
+        new KeyboardTheme(1, R.style.KeyboardTheme_HighContrast),
+        new KeyboardTheme(6, R.style.KeyboardTheme_Stone),
+        new KeyboardTheme(7, R.style.KeyboardTheme_Stone_Bold),
+        new KeyboardTheme(8, R.style.KeyboardTheme_Gingerbread),
+        new KeyboardTheme(5, R.style.KeyboardTheme_IceCreamSandwich),
     };
 
     private SubtypeSwitcher mSubtypeSwitcher;
@@ -354,22 +353,9 @@
             mKeyboardView.closing();
         }
 
-        Utils.GCUtils.getInstance().reset();
-        boolean tryGC = true;
-        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
-            try {
-                setContextThemeWrapper(mLatinIME, mKeyboardTheme);
-                mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
-                        R.layout.input_view, null);
-                tryGC = false;
-            } catch (OutOfMemoryError e) {
-                Log.w(TAG, "load keyboard failed: " + e);
-                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e);
-            } catch (InflateException e) {
-                Log.w(TAG, "load keyboard failed: " + e);
-                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e);
-            }
-        }
+        setContextThemeWrapper(mLatinIME, mKeyboardTheme);
+        mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
+                R.layout.input_view, null);
 
         mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
         if (isHardwareAcceleratedDrawingEnabled) {
@@ -402,4 +388,16 @@
             }
         }
     }
+
+    public int getManualCapsMode() {
+        switch (getKeyboard().mId.mElementId) {
+        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+            return WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED;
+        case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+            return WordComposer.CAPS_MODE_MANUAL_SHIFTED;
+        default:
+            return WordComposer.CAPS_MODE_OFF;
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 0e6de70..127ac71 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -30,6 +30,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Message;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
@@ -38,6 +39,7 @@
 import android.widget.TextView;
 
 import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
@@ -78,8 +80,12 @@
  * @attr ref R.styleable#KeyboardView_shadowRadius
  */
 public class KeyboardView extends View implements PointerTracker.DrawingProxy {
+    private static final String TAG = KeyboardView.class.getSimpleName();
+
     // Miscellaneous constants
     private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
+    private static final float UNDEFINED_RATIO = -1.0f;
+    private static final int UNDEFINED_DIMENSION = -1;
 
     // XML attributes
     protected final float mVerticalCorrection;
@@ -103,23 +109,19 @@
 
     // Key preview
     private final int mKeyPreviewLayoutId;
+    private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray();
     protected final KeyPreviewDrawParams mKeyPreviewDrawParams;
     private boolean mShowKeyPreviewPopup = true;
     private int mDelayAfterPreview;
     private final PreviewPlacerView mPreviewPlacerView;
 
-    /** True if {@link KeyboardView} should handle gesture events. */
-    protected boolean mShouldHandleGesture;
-
     // Drawing
     /** True if the entire keyboard needs to be dimmed. */
     private boolean mNeedsToDimEntireKeyboard;
-    /** Whether the keyboard bitmap buffer needs to be redrawn before it's blitted. **/
-    private boolean mBufferNeedsUpdate;
     /** True if all keys should be drawn */
     private boolean mInvalidateAllKeys;
     /** The keys that should be drawn */
-    private final HashSet<Key> mInvalidatedKeys = new HashSet<Key>();
+    private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet();
     /** The working rectangle variable */
     private final Rect mWorkingRect = new Rect();
     /** The keyboard bitmap buffer for faster updates */
@@ -131,9 +133,9 @@
     private final Paint mPaint = new Paint();
     private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
     // This sparse array caches key label text height in pixel indexed by key label text size.
-    private static final SparseArray<Float> sTextHeightCache = new SparseArray<Float>();
+    private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray();
     // This sparse array caches key label text width in pixel indexed by key label text size.
-    private static final SparseArray<Float> sTextWidthCache = new SparseArray<Float>();
+    private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray();
     private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
     private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
 
@@ -153,7 +155,10 @@
             final PointerTracker tracker = (PointerTracker) msg.obj;
             switch (msg.what) {
             case MSG_DISMISS_KEY_PREVIEW:
-                tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
+                final TextView previewText = keyboardView.mKeyPreviewTexts.get(tracker.mPointerId);
+                if (previewText != null) {
+                    previewText.setVisibility(INVISIBLE);
+                }
                 break;
             }
         }
@@ -166,7 +171,7 @@
             removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
         }
 
-        public void cancelAllDismissKeyPreviews() {
+        private void cancelAllDismissKeyPreviews() {
             removeMessages(MSG_DISMISS_KEY_PREVIEW);
         }
 
@@ -199,7 +204,6 @@
         private final float mKeyHintLetterRatio;
         private final float mKeyShiftedLetterHintRatio;
         private final float mKeyHintLabelRatio;
-        private static final float UNDEFINED_RATIO = -1.0f;
 
         public final Rect mPadding = new Rect();
         public int mKeyLetterSize;
@@ -211,26 +215,22 @@
         public int mKeyHintLabelSize;
         public int mAnimAlpha;
 
-        public KeyDrawParams(TypedArray a) {
+        public KeyDrawParams(final TypedArray a) {
             mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
-            if (a.hasValue(R.styleable.KeyboardView_keyLetterSize)) {
-                mKeyLetterRatio = UNDEFINED_RATIO;
-                mKeyLetterSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLetterSize, 0);
-            } else {
-                mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
+            if (!isValidFraction(mKeyLetterRatio = getFraction(a,
+                    R.styleable.KeyboardView_keyLetterSize))) {
+                mKeyLetterSize = getDimensionPixelSize(a, R.styleable.KeyboardView_keyLetterSize);
             }
-            if (a.hasValue(R.styleable.KeyboardView_keyLabelSize)) {
-                mKeyLabelRatio = UNDEFINED_RATIO;
-                mKeyLabelSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLabelSize, 0);
-            } else {
-                mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
+            if (!isValidFraction(mKeyLabelRatio = getFraction(a,
+                    R.styleable.KeyboardView_keyLabelSize))) {
+                mKeyLabelSize = getDimensionPixelSize(a, R.styleable.KeyboardView_keyLabelSize);
             }
-            mKeyLargeLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLabelRatio);
-            mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio);
-            mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
-            mKeyShiftedLetterHintRatio = getRatio(a,
+            mKeyLargeLabelRatio = getFraction(a, R.styleable.KeyboardView_keyLargeLabelRatio);
+            mKeyLargeLetterRatio = getFraction(a, R.styleable.KeyboardView_keyLargeLetterRatio);
+            mKeyHintLetterRatio = getFraction(a, R.styleable.KeyboardView_keyHintLetterRatio);
+            mKeyShiftedLetterHintRatio = getFraction(a,
                     R.styleable.KeyboardView_keyShiftedLetterHintRatio);
-            mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio);
+            mKeyHintLabelRatio = getFraction(a, R.styleable.KeyboardView_keyHintLabelRatio);
             mKeyLabelHorizontalPadding = a.getDimension(
                     R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
             mKeyHintLetterPadding = a.getDimension(
@@ -257,10 +257,10 @@
         }
 
         public void updateKeyHeight(int keyHeight) {
-            if (mKeyLetterRatio >= 0.0f) {
+            if (isValidFraction(mKeyLetterRatio)) {
                 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
             }
-            if (mKeyLabelRatio >= 0.0f) {
+            if (isValidFraction(mKeyLabelRatio)) {
                 mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
             }
             mKeyLargeLabelSize = (int)(keyHeight * mKeyLargeLabelRatio);
@@ -335,7 +335,7 @@
                     R.styleable.KeyboardView_keyPreviewOffset, 0);
             mPreviewHeight = a.getDimensionPixelSize(
                     R.styleable.KeyboardView_keyPreviewHeight, 80);
-            mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio);
+            mPreviewTextRatio = getFraction(a, R.styleable.KeyboardView_keyPreviewTextRatio);
             mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
             mLingerTimeout = a.getInt(R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
 
@@ -367,9 +367,9 @@
 
         final TypedArray a = context.obtainStyledAttributes(
                 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
-
         mKeyDrawParams = new KeyDrawParams(a);
         mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams);
+        mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout;
         mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
         if (mKeyPreviewLayoutId == 0) {
             mShowKeyPreviewPopup = false;
@@ -378,17 +378,30 @@
                 R.styleable.KeyboardView_verticalCorrection, 0);
         mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0);
         mBackgroundDimAlpha = a.getInt(R.styleable.KeyboardView_backgroundDimAlpha, 0);
-        mPreviewPlacerView = new PreviewPlacerView(context, a);
         a.recycle();
 
-        mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout;
-
+        mPreviewPlacerView = new PreviewPlacerView(context, attrs);
         mPaint.setAntiAlias(true);
     }
 
-    // Read fraction value in TypedArray as float.
-    /* package */ static float getRatio(TypedArray a, int index) {
-        return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
+    static boolean isValidFraction(final float fraction) {
+        return fraction >= 0.0f;
+    }
+
+    static float getFraction(final TypedArray a, final int index) {
+        final TypedValue value = a.peekValue(index);
+        if (value == null || value.type != TypedValue.TYPE_FRACTION) {
+            return UNDEFINED_RATIO;
+        }
+        return a.getFraction(index, 1, 1, UNDEFINED_RATIO);
+    }
+
+    public static int getDimensionPixelSize(final TypedArray a, final int index) {
+        final TypedValue value = a.peekValue(index);
+        if (value == null || value.type != TypedValue.TYPE_DIMENSION) {
+            return UNDEFINED_DIMENSION;
+        }
+        return a.getDimensionPixelSize(index, UNDEFINED_DIMENSION);
     }
 
     /**
@@ -438,9 +451,8 @@
         return mShowKeyPreviewPopup;
     }
 
-    public void setGestureHandlingMode(boolean shouldHandleGesture,
-            boolean drawsGesturePreviewTrail, boolean drawsGestureFloatingPreviewText) {
-        mShouldHandleGesture = shouldHandleGesture;
+    public void setGesturePreviewMode(boolean drawsGesturePreviewTrail,
+            boolean drawsGestureFloatingPreviewText) {
         mPreviewPlacerView.setGesturePreviewMode(
                 drawsGesturePreviewTrail, drawsGestureFloatingPreviewText);
     }
@@ -463,16 +475,12 @@
             onDrawKeyboard(canvas);
             return;
         }
-        if (mBufferNeedsUpdate || mOffscreenBuffer == null) {
-            mBufferNeedsUpdate = false;
+
+        final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty();
+        if (bufferNeedsUpdates || mOffscreenBuffer == null) {
             if (maybeAllocateOffscreenBuffer()) {
                 mInvalidateAllKeys = true;
-                // TODO: Stop using the offscreen canvas even when in software rendering
-                if (mOffscreenCanvas != null) {
-                    mOffscreenCanvas.setBitmap(mOffscreenBuffer);
-                } else {
-                    mOffscreenCanvas = new Canvas(mOffscreenBuffer);
-                }
+                maybeCreateOffscreenCanvas();
             }
             onDrawKeyboard(mOffscreenCanvas);
         }
@@ -501,6 +509,15 @@
         }
     }
 
+    private void maybeCreateOffscreenCanvas() {
+        // TODO: Stop using the offscreen canvas even when in software rendering
+        if (mOffscreenCanvas != null) {
+            mOffscreenCanvas.setBitmap(mOffscreenBuffer);
+        } else {
+            mOffscreenCanvas = new Canvas(mOffscreenBuffer);
+        }
+    }
+
     private void onDrawKeyboard(final Canvas canvas) {
         if (mKeyboard == null) return;
 
@@ -528,13 +545,12 @@
         }
         if (!isHardwareAccelerated) {
             canvas.clipRegion(mClipRegion, Region.Op.REPLACE);
-        }
-
-        // Draw keyboard background.
-        canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
-        final Drawable background = getBackground();
-        if (background != null) {
-            background.draw(canvas);
+            // Draw keyboard background.
+            canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
+            final Drawable background = getBackground();
+            if (background != null) {
+                background.draw(canvas);
+            }
         }
 
         // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
@@ -907,15 +923,30 @@
         }
     }
 
-    // Called by {@link PointerTracker} constructor to create a TextView.
-    @Override
-    public TextView inflateKeyPreviewText() {
+    private TextView getKeyPreviewText(final int pointerId) {
+        TextView previewText = mKeyPreviewTexts.get(pointerId);
+        if (previewText != null) {
+            return previewText;
+        }
         final Context context = getContext();
         if (mKeyPreviewLayoutId != 0) {
-            return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
+            previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
         } else {
-            return new TextView(context);
+            previewText = new TextView(context);
         }
+        mKeyPreviewTexts.put(pointerId, previewText);
+        return previewText;
+    }
+
+    private void dismissAllKeyPreviews() {
+        final int pointerCount = mKeyPreviewTexts.size();
+        for (int id = 0; id < pointerCount; id++) {
+            final TextView previewText = mKeyPreviewTexts.get(id);
+            if (previewText != null) {
+                previewText.setVisibility(INVISIBLE);
+            }
+        }
+        PointerTracker.setReleasedKeyGraphicsToAllKeys();
     }
 
     @Override
@@ -936,9 +967,18 @@
         final int[] viewOrigin = new int[2];
         getLocationInWindow(viewOrigin);
         mPreviewPlacerView.setOrigin(viewOrigin[0], viewOrigin[1]);
-        final ViewGroup windowContentView =
-                (ViewGroup)getRootView().findViewById(android.R.id.content);
-        windowContentView.addView(mPreviewPlacerView);
+        final View rootView = getRootView();
+        if (rootView == null) {
+            Log.w(TAG, "Cannot find root view");
+            return;
+        }
+        final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content);
+        // Note: It'd be very weird if we get null by android.R.id.content.
+        if (windowContentView == null) {
+            Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView");
+        } else {
+            windowContentView.addView(mPreviewPlacerView);
+        }
     }
 
     public void showGestureFloatingPreviewText(String gestureFloatingPreviewText) {
@@ -952,7 +992,7 @@
     }
 
     @Override
-    public void showGestureTrail(PointerTracker tracker) {
+    public void showGesturePreviewTrail(PointerTracker tracker) {
         locatePreviewPlacerView();
         mPreviewPlacerView.invalidatePointer(tracker);
     }
@@ -962,7 +1002,7 @@
     public void showKeyPreview(PointerTracker tracker) {
         if (!mShowKeyPreviewPopup) return;
 
-        final TextView previewText = tracker.getKeyPreviewText();
+        final TextView previewText = getKeyPreviewText(tracker.mPointerId);
         // If the key preview has no parent view yet, add it to the ViewGroup which can place
         // key preview absolutely in SoftInputWindow.
         if (previewText.getParent() == null) {
@@ -1052,7 +1092,6 @@
     public void invalidateAllKeys() {
         mInvalidatedKeys.clear();
         mInvalidateAllKeys = true;
-        mBufferNeedsUpdate = true;
         invalidate();
     }
 
@@ -1070,13 +1109,11 @@
         mInvalidatedKeys.add(key);
         final int x = key.mX + getPaddingLeft();
         final int y = key.mY + getPaddingTop();
-        mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight);
-        mBufferNeedsUpdate = true;
-        invalidate(mWorkingRect);
+        invalidate(x, y, x + key.mWidth, y + key.mHeight);
     }
 
     public void closing() {
-        PointerTracker.dismissAllKeyPreviews();
+        dismissAllKeyPreviews();
         cancelAllMessages();
 
         mInvalidateAllKeys = true;
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index fe9cb94..df84271 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -110,7 +110,6 @@
             new WeakHashMap<Key, MoreKeysPanel>();
     private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
 
-    private final PointerTrackerParams mPointerTrackerParams;
     private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
 
     protected KeyDetector mKeyDetector;
@@ -127,11 +126,26 @@
         private static final int MSG_LONGPRESS_KEY = 2;
         private static final int MSG_DOUBLE_TAP = 3;
 
-        private final KeyTimerParams mParams;
+        private final int mKeyRepeatStartTimeout;
+        private final int mKeyRepeatInterval;
+        private final int mLongPressKeyTimeout;
+        private final int mLongPressShiftKeyTimeout;
+        private final int mIgnoreAltCodeKeyTimeout;
 
-        public KeyTimerHandler(MainKeyboardView outerInstance, KeyTimerParams params) {
+        public KeyTimerHandler(final MainKeyboardView outerInstance,
+                final TypedArray mainKeyboardViewAttr) {
             super(outerInstance);
-            mParams = params;
+
+            mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
+            mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_keyRepeatInterval, 0);
+            mLongPressKeyTimeout = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_longPressKeyTimeout, 0);
+            mLongPressShiftKeyTimeout = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0);
+            mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
         }
 
         @Override
@@ -146,7 +160,7 @@
                 final Key currentKey = tracker.getKey();
                 if (currentKey != null && currentKey.mCode == msg.arg1) {
                     tracker.onRegisterKey(currentKey);
-                    startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval);
+                    startKeyRepeatTimer(tracker, mKeyRepeatInterval);
                 }
                 break;
             case MSG_LONGPRESS_KEY:
@@ -167,7 +181,7 @@
 
         @Override
         public void startKeyRepeatTimer(PointerTracker tracker) {
-            startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout);
+            startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout);
         }
 
         public void cancelKeyRepeatTimer() {
@@ -185,7 +199,7 @@
             final int delay;
             switch (code) {
             case Keyboard.CODE_SHIFT:
-                delay = mParams.mLongPressShiftKeyTimeout;
+                delay = mLongPressShiftKeyTimeout;
                 break;
             default:
                 delay = 0;
@@ -206,15 +220,15 @@
             final int delay;
             switch (key.mCode) {
             case Keyboard.CODE_SHIFT:
-                delay = mParams.mLongPressShiftKeyTimeout;
+                delay = mLongPressShiftKeyTimeout;
                 break;
             default:
                 if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
                     // We use longer timeout for sliding finger input started from the symbols
                     // mode key.
-                    delay = mParams.mLongPressKeyTimeout * 3;
+                    delay = mLongPressKeyTimeout * 3;
                 } else {
-                    delay = mParams.mLongPressKeyTimeout;
+                    delay = mLongPressKeyTimeout;
                 }
                 break;
             }
@@ -268,7 +282,7 @@
             }
 
             sendMessageDelayed(
-                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout);
+                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
             if (isTyping) {
                 return;
             }
@@ -307,50 +321,6 @@
         }
     }
 
-    public static class PointerTrackerParams {
-        public final boolean mSlidingKeyInputEnabled;
-        public final int mTouchNoiseThresholdTime;
-        public final float mTouchNoiseThresholdDistance;
-
-        public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
-
-        private PointerTrackerParams() {
-            mSlidingKeyInputEnabled = false;
-            mTouchNoiseThresholdTime =0;
-            mTouchNoiseThresholdDistance = 0;
-        }
-
-        public PointerTrackerParams(TypedArray mainKeyboardViewAttr) {
-            mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean(
-                    R.styleable.MainKeyboardView_slidingKeyInputEnable, false);
-            mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0);
-            mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimension(
-                    R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0);
-        }
-    }
-
-    static class KeyTimerParams {
-        public final int mKeyRepeatStartTimeout;
-        public final int mKeyRepeatInterval;
-        public final int mLongPressKeyTimeout;
-        public final int mLongPressShiftKeyTimeout;
-        public final int mIgnoreAltCodeKeyTimeout;
-
-        public KeyTimerParams(TypedArray mainKeyboardViewAttr) {
-            mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
-            mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_keyRepeatInterval, 0);
-            mLongPressKeyTimeout = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_longPressKeyTimeout, 0);
-            mLongPressShiftKeyTimeout = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0);
-            mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
-        }
-    }
-
     public MainKeyboardView(Context context, AttributeSet attrs) {
         this(context, attrs, R.attr.mainKeyboardViewStyle);
     }
@@ -374,8 +344,8 @@
                 R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false);
         mAutoCorrectionSpacebarLedIcon = a.getDrawable(
                 R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon);
-        mSpacebarTextRatio = a.getFraction(R.styleable.MainKeyboardView_spacebarTextRatio,
-                1000, 1000, 1) / 1000.0f;
+        mSpacebarTextRatio = a.getFraction(
+                R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f);
         mSpacebarTextColor = a.getColor(R.styleable.MainKeyboardView_spacebarTextColor, 0);
         mSpacebarTextShadowColor = a.getColor(
                 R.styleable.MainKeyboardView_spacebarTextShadowColor, 0);
@@ -389,19 +359,15 @@
         final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId(
                 R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
 
-        final KeyTimerParams keyTimerParams = new KeyTimerParams(a);
-        mPointerTrackerParams = new PointerTrackerParams(a);
-
         final float keyHysteresisDistance = a.getDimension(
                 R.styleable.MainKeyboardView_keyHysteresisDistance, 0);
         mKeyDetector = new KeyDetector(keyHysteresisDistance);
-        mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams);
+        mKeyTimerHandler = new KeyTimerHandler(this, a);
         mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean(
                 R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
+        PointerTracker.setParameters(a);
         a.recycle();
 
-        PointerTracker.setParameters(mPointerTrackerParams);
-
         mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
                 languageOnSpacebarFadeoutAnimatorResId, this);
         mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
@@ -482,7 +448,7 @@
         super.setKeyboard(keyboard);
         mKeyDetector.setKeyboard(
                 keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
-        PointerTracker.setKeyDetector(mKeyDetector, mShouldHandleGesture);
+        PointerTracker.setKeyDetector(mKeyDetector);
         mTouchScreenRegulator.setKeyboard(keyboard);
         mMoreKeysPanelCache.clear();
 
@@ -500,12 +466,13 @@
         AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard);
     }
 
-    @Override
-    public void setGestureHandlingMode(final boolean shouldHandleGesture,
-            boolean drawsGesturePreviewTrail, boolean drawsGestureFloatingPreviewText) {
-        super.setGestureHandlingMode(shouldHandleGesture, drawsGesturePreviewTrail,
-                drawsGestureFloatingPreviewText);
-        PointerTracker.setKeyDetector(mKeyDetector, shouldHandleGesture);
+    // Note that this method is called from a non-UI thread.
+    public void setMainDictionaryAvailability(boolean mainDictionaryAvailable) {
+        PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable);
+    }
+
+    public void setGestureHandlingEnabledByUser(boolean gestureHandlingEnabledByUser) {
+        PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser);
     }
 
     /**
@@ -527,7 +494,17 @@
         // to properly show the splash screen, which requires that the window token of the
         // KeyboardView be non-null.
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow();
+            ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(this);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        // Notify the research logger that the keyboard view has been detached.  This is needed
+        // to invalidate the reference of {@link MainKeyboardView} to null.
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().mainKeyboardView_onDetachedFromWindow();
         }
     }
 
@@ -607,9 +584,8 @@
     }
 
     private void invokeCodeInput(int primaryCode) {
-        mKeyboardActionListener.onCodeInput(primaryCode,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+        mKeyboardActionListener.onCodeInput(
+                primaryCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
     }
 
     private void invokeReleaseKey(int primaryCode) {
@@ -834,20 +810,6 @@
         return false;
     }
 
-    @Override
-    public void draw(Canvas c) {
-        Utils.GCUtils.getInstance().reset();
-        boolean tryGC = true;
-        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
-            try {
-                super.draw(c);
-                tryGC = false;
-            } catch (OutOfMemoryError e) {
-                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(TAG, e);
-            }
-        }
-    }
-
     /**
      * Receives hover events from the input framework.
      *
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
index a183546..cd4e300 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
@@ -39,11 +39,7 @@
 
         Key nearestKey = null;
         int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
-        final Keyboard keyboard = getKeyboard();
-        if (keyboard == null) {
-            throw new NullPointerException("Keyboard isn't set");
-        }
-        for (final Key key : keyboard.mKeys) {
+        for (final Key key : getKeyboard().mKeys) {
             final int dist = key.squaredDistanceToEdge(touchX, touchY);
             if (dist < nearestDist) {
                 nearestKey = key;
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index 870eff2..e513a14 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -25,6 +25,7 @@
 
 import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
 import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.InputPointers;
 import com.android.inputmethod.latin.R;
 
@@ -50,7 +51,8 @@
         public void onCodeInput(int primaryCode, int x, int y) {
             // Because a more keys keyboard doesn't need proximity characters correction, we don't
             // send touch event coordinates.
-            mListener.onCodeInput(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE);
+            mListener.onCodeInput(
+                    primaryCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         }
 
         @Override
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 184011f..be101cf 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -16,25 +16,25 @@
 
 package com.android.inputmethod.keyboard;
 
-import android.graphics.Canvas;
-import android.graphics.Paint;
+import android.content.res.TypedArray;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.MotionEvent;
-import android.view.View;
-import android.widget.TextView;
 
 import com.android.inputmethod.accessibility.AccessibilityUtils;
 import com.android.inputmethod.keyboard.internal.GestureStroke;
+import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewTrail;
 import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.InputPointers;
 import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.research.ResearchLogger;
 
 import java.util.ArrayList;
 
-public class PointerTracker implements PointerTrackerQueue.ElementActions {
+public class PointerTracker implements PointerTrackerQueue.Element {
     private static final String TAG = PointerTracker.class.getSimpleName();
     private static final boolean DEBUG_EVENT = false;
     private static final boolean DEBUG_MOVE_EVENT = false;
@@ -43,6 +43,9 @@
 
     /** True if {@link PointerTracker}s should handle gesture events. */
     private static boolean sShouldHandleGesture = false;
+    private static boolean sMainDictionaryAvailable = false;
+    private static boolean sGestureHandlingEnabledByInputField = false;
+    private static boolean sGestureHandlingEnabledByUser = false;
 
     private static final int MIN_GESTURE_RECOGNITION_TIME = 100; // msec
 
@@ -75,10 +78,9 @@
 
     public interface DrawingProxy extends MoreKeysPanel.Controller {
         public void invalidateKey(Key key);
-        public TextView inflateKeyPreviewText();
         public void showKeyPreview(PointerTracker tracker);
         public void dismissKeyPreview(PointerTracker tracker);
-        public void showGestureTrail(PointerTracker tracker);
+        public void showGesturePreviewTrail(PointerTracker tracker);
     }
 
     public interface TimerProxy {
@@ -117,19 +119,40 @@
         }
     }
 
+    static class PointerTrackerParams {
+        public final boolean mSlidingKeyInputEnabled;
+        public final int mTouchNoiseThresholdTime;
+        public final float mTouchNoiseThresholdDistance;
+        public final int mTouchNoiseThresholdDistanceSquared;
+
+        public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
+
+        private PointerTrackerParams() {
+            mSlidingKeyInputEnabled = false;
+            mTouchNoiseThresholdTime = 0;
+            mTouchNoiseThresholdDistance = 0.0f;
+            mTouchNoiseThresholdDistanceSquared = 0;
+        }
+
+        public PointerTrackerParams(TypedArray mainKeyboardViewAttr) {
+            mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean(
+                    R.styleable.MainKeyboardView_slidingKeyInputEnable, false);
+            mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0);
+            final float touchNouseThresholdDistance = mainKeyboardViewAttr.getDimension(
+                    R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0);
+            mTouchNoiseThresholdDistance = touchNouseThresholdDistance;
+            mTouchNoiseThresholdDistanceSquared =
+                    (int)(touchNouseThresholdDistance * touchNouseThresholdDistance);
+        }
+    }
+
     // Parameters for pointer handling.
-    private static MainKeyboardView.PointerTrackerParams sParams;
-    private static int sTouchNoiseThresholdDistanceSquared;
+    private static PointerTrackerParams sParams;
     private static boolean sNeedsPhantomSuddenMoveEventHack;
 
-    private static final ArrayList<PointerTracker> sTrackers = new ArrayList<PointerTracker>();
-    private static final InputPointers sAggregratedPointers = new InputPointers(
-            GestureStroke.DEFAULT_CAPACITY);
+    private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList();
     private static PointerTrackerQueue sPointerTrackerQueue;
-    // HACK: Change gesture detection criteria depending on this variable.
-    // TODO: Find more comprehensive ways to detect a gesture start.
-    // True when the previous user input was a gesture input, not a typing input.
-    private static boolean sWasInGesture;
 
     public final int mPointerId;
 
@@ -140,15 +163,14 @@
 
     private Keyboard mKeyboard;
     private int mKeyQuarterWidthSquared;
-    private final TextView mKeyPreviewText;
 
-    private boolean mIsAlphabetKeyboard;
-    private boolean mIsPossibleGesture = false;
-    private boolean mInGesture = false;
-
-    // TODO: Remove these variables
-    private int mLastRecognitionPointSize = 0;
-    private long mLastRecognitionTime = 0;
+    private boolean mIsDetectingGesture = false; // per PointerTracker.
+    private static boolean sInGesture = false;
+    private static long sGestureFirstDownTime;
+    private static final InputPointers sAggregratedPointers = new InputPointers(
+            GestureStroke.DEFAULT_CAPACITY);
+    private static int sLastRecognitionPointSize = 0;
+    private static long sLastRecognitionTime = 0;
 
     // The position and time at which first down event occurred.
     private long mDownTime;
@@ -186,7 +208,7 @@
     private static final KeyboardActionListener EMPTY_LISTENER =
             new KeyboardActionListener.Adapter();
 
-    private final GestureStroke mGestureStroke;
+    private final GestureStrokeWithPreviewTrail mGestureStrokeWithPreviewTrail;
 
     public static void init(boolean hasDistinctMultitouch,
             boolean needsPhantomSuddenMoveEventHack) {
@@ -196,28 +218,32 @@
             sPointerTrackerQueue = null;
         }
         sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
-
-        setParameters(MainKeyboardView.PointerTrackerParams.DEFAULT);
-        updateGestureHandlingMode(null, false /* shouldHandleGesture */);
+        sParams = PointerTrackerParams.DEFAULT;
     }
 
-    public static void setParameters(MainKeyboardView.PointerTrackerParams params) {
-        sParams = params;
-        sTouchNoiseThresholdDistanceSquared = (int)(
-                params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance);
+    public static void setParameters(final TypedArray mainKeyboardViewAttr) {
+        sParams = new PointerTrackerParams(mainKeyboardViewAttr);
     }
 
-    private static void updateGestureHandlingMode(Keyboard keyboard, boolean shouldHandleGesture) {
-        if (!shouldHandleGesture
-                || AccessibilityUtils.getInstance().isTouchExplorationEnabled()
-                || (keyboard != null && keyboard.mId.passwordInput())) {
-            sShouldHandleGesture = false;
-        } else {
-            sShouldHandleGesture = true;
-        }
+    private static void updateGestureHandlingMode() {
+        sShouldHandleGesture = sMainDictionaryAvailable
+                && sGestureHandlingEnabledByInputField
+                && sGestureHandlingEnabledByUser
+                && !AccessibilityUtils.getInstance().isTouchExplorationEnabled();
     }
 
-    public static PointerTracker getPointerTracker(final int id, KeyEventHandler handler) {
+    // Note that this method is called from a non-UI thread.
+    public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
+        sMainDictionaryAvailable = mainDictionaryAvailable;
+        updateGestureHandlingMode();
+    }
+
+    public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
+        sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser;
+        updateGestureHandlingMode();
+    }
+
+    public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) {
         final ArrayList<PointerTracker> trackers = sTrackers;
 
         // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
@@ -233,7 +259,7 @@
         return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false;
     }
 
-    public static void setKeyboardActionListener(KeyboardActionListener listener) {
+    public static void setKeyboardActionListener(final KeyboardActionListener listener) {
         final int trackersSize = sTrackers.size();
         for (int i = 0; i < trackersSize; ++i) {
             final PointerTracker tracker = sTrackers.get(i);
@@ -241,7 +267,7 @@
         }
     }
 
-    public static void setKeyDetector(KeyDetector keyDetector, boolean shouldHandleGesture) {
+    public static void setKeyDetector(final KeyDetector keyDetector) {
         final int trackersSize = sTrackers.size();
         for (int i = 0; i < trackersSize; ++i) {
             final PointerTracker tracker = sTrackers.get(i);
@@ -250,70 +276,33 @@
             tracker.mKeyboardLayoutHasBeenChanged = true;
         }
         final Keyboard keyboard = keyDetector.getKeyboard();
-        updateGestureHandlingMode(keyboard, shouldHandleGesture);
+        sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput();
+        updateGestureHandlingMode();
     }
 
-    public static void dismissAllKeyPreviews() {
+    public static void setReleasedKeyGraphicsToAllKeys() {
         final int trackersSize = sTrackers.size();
         for (int i = 0; i < trackersSize; ++i) {
             final PointerTracker tracker = sTrackers.get(i);
-            tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
             tracker.setReleasedKeyGraphics(tracker.mCurrentKey);
         }
     }
 
-    // TODO: To handle multi-touch gestures we may want to move this method to
-    // {@link PointerTrackerQueue}.
-    private static InputPointers getIncrementalBatchPoints() {
-        final int trackersSize = sTrackers.size();
-        for (int i = 0; i < trackersSize; ++i) {
-            final PointerTracker tracker = sTrackers.get(i);
-            tracker.mGestureStroke.appendIncrementalBatchPoints(sAggregratedPointers);
-        }
-        return sAggregratedPointers;
-    }
-
-    // TODO: To handle multi-touch gestures we may want to move this method to
-    // {@link PointerTrackerQueue}.
-    private static InputPointers getAllBatchPoints() {
-        final int trackersSize = sTrackers.size();
-        for (int i = 0; i < trackersSize; ++i) {
-            final PointerTracker tracker = sTrackers.get(i);
-            tracker.mGestureStroke.appendAllBatchPoints(sAggregratedPointers);
-        }
-        return sAggregratedPointers;
-    }
-
-    // TODO: To handle multi-touch gestures we may want to move this method to
-    // {@link PointerTrackerQueue}.
-    public static void clearBatchInputPointsOfAllPointerTrackers() {
-        final int trackersSize = sTrackers.size();
-        for (int i = 0; i < trackersSize; ++i) {
-            final PointerTracker tracker = sTrackers.get(i);
-            tracker.mGestureStroke.reset();
-        }
-        sAggregratedPointers.reset();
-    }
-
-    private PointerTracker(int id, KeyEventHandler handler) {
-        if (handler == null)
+    private PointerTracker(final int id, final KeyEventHandler handler) {
+        if (handler == null) {
             throw new NullPointerException();
+        }
         mPointerId = id;
-        mGestureStroke = new GestureStroke(id);
+        mGestureStrokeWithPreviewTrail = new GestureStrokeWithPreviewTrail(id);
         setKeyDetectorInner(handler.getKeyDetector());
         mListener = handler.getKeyboardActionListener();
         mDrawingProxy = handler.getDrawingProxy();
         mTimerProxy = handler.getTimerProxy();
-        mKeyPreviewText = mDrawingProxy.inflateKeyPreviewText();
-    }
-
-    public TextView getKeyPreviewText() {
-        return mKeyPreviewText;
     }
 
     // Returns true if keyboard has been changed by this callback.
-    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) {
-        if (mInGesture) {
+    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
+        if (sInGesture) {
             return false;
         }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
@@ -337,7 +326,8 @@
 
     // Note that we need primaryCode argument because the keyboard may in shifted state and the
     // primaryCode is different from {@link Key#mCode}.
-    private void callListenerOnCodeInput(Key key, int primaryCode, int x, int y) {
+    private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
+            final int y) {
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
         final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
         final int code = altersCode ? key.mAltCode : primaryCode;
@@ -366,8 +356,9 @@
 
     // Note that we need primaryCode argument because the keyboard may in shifted state and the
     // primaryCode is different from {@link Key#mCode}.
-    private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
-        if (mInGesture) {
+    private void callListenerOnRelease(final Key key, final int primaryCode,
+            final boolean withSliding) {
+        if (sInGesture) {
             return;
         }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
@@ -389,20 +380,19 @@
     }
 
     private void callListenerOnCancelInput() {
-        if (DEBUG_LISTENER)
+        if (DEBUG_LISTENER) {
             Log.d(TAG, "onCancelInput");
+        }
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.pointerTracker_callListenerOnCancelInput();
         }
         mListener.onCancelInput();
     }
 
-    private void setKeyDetectorInner(KeyDetector keyDetector) {
+    private void setKeyDetectorInner(final KeyDetector keyDetector) {
         mKeyDetector = keyDetector;
         mKeyboard = keyDetector.getKeyboard();
-        mIsAlphabetKeyboard = mKeyboard.mId.isAlphabetKeyboard();
-        mGestureStroke.setGestureSampleLength(
-                mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
+        mGestureStrokeWithPreviewTrail.setGestureSampleLength(mKeyboard.mMostCommonKeyWidth);
         final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY);
         if (newKey != mCurrentKey) {
             if (mDrawingProxy != null) {
@@ -428,11 +418,11 @@
         return mCurrentKey != null && mCurrentKey.isModifier();
     }
 
-    public Key getKeyOn(int x, int y) {
+    public Key getKeyOn(final int x, final int y) {
         return mKeyDetector.detectHitKey(x, y);
     }
 
-    private void setReleasedKeyGraphics(Key key) {
+    private void setReleasedKeyGraphics(final Key key) {
         mDrawingProxy.dismissKeyPreview(this);
         if (key == null) {
             return;
@@ -463,7 +453,7 @@
         }
     }
 
-    private void setPressedKeyGraphics(Key key) {
+    private void setPressedKeyGraphics(final Key key) {
         if (key == null) {
             return;
         }
@@ -475,7 +465,7 @@
             return;
         }
 
-        if (!key.noKeyPreview() && !mInGesture) {
+        if (!key.noKeyPreview() && !sInGesture) {
             mDrawingProxy.showKeyPreview(this);
         }
         updatePressKeyGraphics(key);
@@ -502,20 +492,18 @@
         }
     }
 
-    private void updateReleaseKeyGraphics(Key key) {
+    private void updateReleaseKeyGraphics(final Key key) {
         key.onReleased();
         mDrawingProxy.invalidateKey(key);
     }
 
-    private void updatePressKeyGraphics(Key key) {
+    private void updatePressKeyGraphics(final Key key) {
         key.onPressed();
         mDrawingProxy.invalidateKey(key);
     }
 
-    public void drawGestureTrail(Canvas canvas, Paint paint) {
-        if (mInGesture) {
-            mGestureStroke.drawGestureTrail(canvas, paint, mLastX, mLastY);
-        }
+    public GestureStrokeWithPreviewTrail getGestureStrokeWithPreviewTrail() {
+        return mGestureStrokeWithPreviewTrail;
     }
 
     public int getLastX() {
@@ -530,77 +518,91 @@
         return mDownTime;
     }
 
-    private Key onDownKey(int x, int y, long eventTime) {
+    private Key onDownKey(final int x, final int y, final long eventTime) {
         mDownTime = eventTime;
         return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
     }
 
-    private Key onMoveKeyInternal(int x, int y) {
+    private Key onMoveKeyInternal(final int x, final int y) {
         mLastX = x;
         mLastY = y;
         return mKeyDetector.detectHitKey(x, y);
     }
 
-    private Key onMoveKey(int x, int y) {
+    private Key onMoveKey(final int x, final int y) {
         return onMoveKeyInternal(x, y);
     }
 
-    private Key onMoveToNewKey(Key newKey, int x, int y) {
+    private Key onMoveToNewKey(final Key newKey, final int x, final int y) {
         mCurrentKey = newKey;
         mKeyX = x;
         mKeyY = y;
         return newKey;
     }
 
+    private static int getActivePointerTrackerCount() {
+        return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size();
+    }
+
     private void startBatchInput() {
         if (DEBUG_LISTENER) {
             Log.d(TAG, "onStartBatchInput");
         }
-        mInGesture = true;
+        sInGesture = true;
         mListener.onStartBatchInput();
+        mDrawingProxy.showGesturePreviewTrail(this);
     }
 
-    private void updateBatchInput(InputPointers batchPoints) {
-        if (DEBUG_LISTENER) {
-            Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize());
+    private void updateBatchInput(final long eventTime) {
+        synchronized (sAggregratedPointers) {
+            mGestureStrokeWithPreviewTrail.appendIncrementalBatchPoints(sAggregratedPointers);
+            final int size = sAggregratedPointers.getPointerSize();
+            if (size > sLastRecognitionPointSize
+                    && eventTime > sLastRecognitionTime + MIN_GESTURE_RECOGNITION_TIME) {
+                sLastRecognitionPointSize = size;
+                sLastRecognitionTime = eventTime;
+                if (DEBUG_LISTENER) {
+                    Log.d(TAG, "onUpdateBatchInput: batchPoints=" + size);
+                }
+                mListener.onUpdateBatchInput(sAggregratedPointers);
+            }
         }
-        mListener.onUpdateBatchInput(batchPoints);
+        mDrawingProxy.showGesturePreviewTrail(this);
     }
 
-    private void endBatchInput(InputPointers batchPoints) {
-        if (DEBUG_LISTENER) {
-            Log.d(TAG, "onEndBatchInput: batchPoints=" + batchPoints.getPointerSize());
+    private void endBatchInput() {
+        synchronized (sAggregratedPointers) {
+            mGestureStrokeWithPreviewTrail.appendAllBatchPoints(sAggregratedPointers);
+            if (getActivePointerTrackerCount() == 1) {
+                if (DEBUG_LISTENER) {
+                    Log.d(TAG, "onEndBatchInput: batchPoints="
+                            + sAggregratedPointers.getPointerSize());
+                }
+                sInGesture = false;
+                mListener.onEndBatchInput(sAggregratedPointers);
+                clearBatchInputPointsOfAllPointerTrackers();
+            }
         }
-        mListener.onEndBatchInput(batchPoints);
-        clearBatchInputRecognitionStateOfThisPointerTracker();
-        clearBatchInputPointsOfAllPointerTrackers();
-        sWasInGesture = true;
+        mDrawingProxy.showGesturePreviewTrail(this);
     }
 
-    private void abortBatchInput() {
-        clearBatchInputRecognitionStateOfThisPointerTracker();
+    private static void abortBatchInput() {
         clearBatchInputPointsOfAllPointerTrackers();
     }
 
-    private void clearBatchInputRecognitionStateOfThisPointerTracker() {
-        mIsPossibleGesture = false;
-        mInGesture = false;
-        mLastRecognitionPointSize = 0;
-        mLastRecognitionTime = 0;
-    }
-
-    private boolean updateBatchInputRecognitionState(long eventTime, int size) {
-        if (size > mLastRecognitionPointSize
-                && eventTime > mLastRecognitionTime + MIN_GESTURE_RECOGNITION_TIME) {
-            mLastRecognitionPointSize = size;
-            mLastRecognitionTime = eventTime;
-            return true;
+    private static void clearBatchInputPointsOfAllPointerTrackers() {
+        final int trackersSize = sTrackers.size();
+        for (int i = 0; i < trackersSize; ++i) {
+            final PointerTracker tracker = sTrackers.get(i);
+            tracker.mGestureStrokeWithPreviewTrail.reset();
         }
-        return false;
+        sAggregratedPointers.reset();
+        sLastRecognitionPointSize = 0;
+        sLastRecognitionTime = 0;
     }
 
-    public void processMotionEvent(int action, int x, int y, long eventTime,
-            KeyEventHandler handler) {
+    public void processMotionEvent(final int action, final int x, final int y, final long eventTime,
+            final KeyEventHandler handler) {
         switch (action) {
         case MotionEvent.ACTION_DOWN:
         case MotionEvent.ACTION_POINTER_DOWN:
@@ -619,9 +621,11 @@
         }
     }
 
-    public void onDownEvent(int x, int y, long eventTime, KeyEventHandler handler) {
-        if (DEBUG_EVENT)
+    public void onDownEvent(final int x, final int y, final long eventTime,
+            final KeyEventHandler handler) {
+        if (DEBUG_EVENT) {
             printTouchEvent("onDownEvent:", x, y, eventTime);
+        }
 
         mDrawingProxy = handler.getDrawingProxy();
         mTimerProxy = handler.getTimerProxy();
@@ -633,7 +637,7 @@
             final int dx = x - mLastX;
             final int dy = y - mLastY;
             final int distanceSquared = (dx * dx + dy * dy);
-            if (distanceSquared < sTouchNoiseThresholdDistanceSquared) {
+            if (distanceSquared < sParams.mTouchNoiseThresholdDistanceSquared) {
                 if (DEBUG_MODE)
                     Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT
                             + " distance=" + distanceSquared);
@@ -645,8 +649,8 @@
             }
         }
 
-        final PointerTrackerQueue queue = sPointerTrackerQueue;
         final Key key = getKeyOn(x, y);
+        final PointerTrackerQueue queue = sPointerTrackerQueue;
         if (queue != null) {
             if (key != null && key.isModifier()) {
                 // Before processing a down event of modifier key, all pointers already being
@@ -656,20 +660,30 @@
             queue.add(this);
         }
         onDownEventInternal(x, y, eventTime);
-        if (queue != null && queue.size() == 1) {
-            mIsPossibleGesture = false;
+        if (!sShouldHandleGesture) {
+            return;
+        }
+        final int activePointerTrackerCount = getActivePointerTrackerCount();
+        if (activePointerTrackerCount == 1) {
+            mIsDetectingGesture = false;
             // A gesture should start only from the letter key.
-            if (sShouldHandleGesture && mIsAlphabetKeyboard && !mIsShowingMoreKeysPanel
-                    && key != null && Keyboard.isLetterCode(key.mCode)) {
-                mIsPossibleGesture = true;
-                // TODO: pointer times should be relative to first down even in entire batch input
-                // instead of resetting to 0 for each new down event.
-                mGestureStroke.addPoint(x, y, 0, false);
+            final boolean isAlphabetKeyboard = (mKeyboard != null)
+                    && mKeyboard.mId.isAlphabetKeyboard();
+            if (isAlphabetKeyboard && !mIsShowingMoreKeysPanel && key != null
+                    && Keyboard.isLetterCode(key.mCode)) {
+                mIsDetectingGesture = true;
+                sGestureFirstDownTime = eventTime;
+                mGestureStrokeWithPreviewTrail.addPoint(x, y, 0, false /* isHistorical */);
             }
+        } else if (sInGesture && activePointerTrackerCount > 1) {
+            mIsDetectingGesture = true;
+            final int elapsedTimeFromFirstDown = (int)(eventTime - sGestureFirstDownTime);
+            mGestureStrokeWithPreviewTrail.addPoint(x, y, elapsedTimeFromFirstDown,
+                    false /* isHistorical */);
         }
     }
 
-    private void onDownEventInternal(int x, int y, long eventTime) {
+    private void onDownEventInternal(final int x, final int y, final long eventTime) {
         Key key = onDownKey(x, y, eventTime);
         // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
         // from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
@@ -694,40 +708,38 @@
         }
     }
 
-    private void startSlidingKeyInput(Key key) {
+    private void startSlidingKeyInput(final Key key) {
         if (!mIsInSlidingKeyInput) {
             mIgnoreModifierKey = key.isModifier();
         }
         mIsInSlidingKeyInput = true;
     }
 
-    private void onGestureMoveEvent(PointerTracker tracker, int x, int y, long eventTime,
-            boolean isHistorical, Key key) {
-        final int gestureTime = (int)(eventTime - tracker.getDownTime());
-        if (sShouldHandleGesture && mIsPossibleGesture) {
-            final GestureStroke stroke = mGestureStroke;
+    private void onGestureMoveEvent(final int x, final int y, final long eventTime,
+            final boolean isHistorical, final Key key) {
+        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
+        if (mIsDetectingGesture) {
+            final GestureStroke stroke = mGestureStrokeWithPreviewTrail;
             stroke.addPoint(x, y, gestureTime, isHistorical);
-            if (!mInGesture && stroke.isStartOfAGesture(gestureTime, sWasInGesture)) {
+            if (!sInGesture && stroke.isStartOfAGesture()) {
                 startBatchInput();
             }
-        }
 
-        if (key != null && mInGesture) {
-            final InputPointers batchPoints = getIncrementalBatchPoints();
-            mDrawingProxy.showGestureTrail(this);
-            if (updateBatchInputRecognitionState(eventTime, batchPoints.getPointerSize())) {
-                updateBatchInput(batchPoints);
+            if (sInGesture && key != null) {
+                updateBatchInput(eventTime);
             }
         }
     }
 
-    public void onMoveEvent(int x, int y, long eventTime, MotionEvent me) {
-        if (DEBUG_MOVE_EVENT)
+    public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
+        if (DEBUG_MOVE_EVENT) {
             printTouchEvent("onMoveEvent:", x, y, eventTime);
-        if (mKeyAlreadyProcessed)
+        }
+        if (mKeyAlreadyProcessed) {
             return;
+        }
 
-        if (me != null) {
+        if (sShouldHandleGesture && me != null) {
             // Add historical points to gesture path.
             final int pointerIndex = me.findPointerIndex(mPointerId);
             final int historicalSize = me.getHistorySize();
@@ -735,24 +747,31 @@
                 final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
                 final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
                 final long historicalTime = me.getHistoricalEventTime(h);
-                onGestureMoveEvent(this, historicalX, historicalY, historicalTime,
+                onGestureMoveEvent(historicalX, historicalY, historicalTime,
                         true /* isHistorical */, null);
             }
         }
 
+        onMoveEventInternal(x, y, eventTime);
+    }
+
+    private void onMoveEventInternal(final int x, final int y, final long eventTime) {
         final int lastX = mLastX;
         final int lastY = mLastY;
         final Key oldKey = mCurrentKey;
         Key key = onMoveKey(x, y);
 
-        // Register move event on gesture tracker.
-        onGestureMoveEvent(this, x, y, eventTime, false /* isHistorical */, key);
-        if (mInGesture) {
-            mIgnoreModifierKey = true;
-            mTimerProxy.cancelLongPressTimer();
-            mIsInSlidingKeyInput = true;
-            mCurrentKey = null;
-            setReleasedKeyGraphics(oldKey);
+        if (sShouldHandleGesture) {
+            // Register move event on gesture tracker.
+            onGestureMoveEvent(x, y, eventTime, false /* isHistorical */, key);
+            if (sInGesture) {
+                mIgnoreModifierKey = true;
+                mTimerProxy.cancelLongPressTimer();
+                mIsInSlidingKeyInput = true;
+                mCurrentKey = null;
+                setReleasedKeyGraphics(oldKey);
+                return;
+            }
         }
 
         if (key != null) {
@@ -797,7 +816,7 @@
                     // TODO: Should find a way to balance gesture detection and this hack.
                     if (sNeedsPhantomSuddenMoveEventHack
                             && lastMoveSquared >= mKeyQuarterWidthSquared
-                            && !mIsPossibleGesture) {
+                            && !mIsDetectingGesture) {
                         if (DEBUG_MODE) {
                             Log.w(TAG, String.format("onMoveEvent:"
                                     + " phantom sudden move event is translated to "
@@ -815,11 +834,11 @@
                         // touch panels when there are close multiple touches.
                         // Caveat: When in chording input mode with a modifier key, we don't use
                         // this hack.
-                        if (me != null && me.getPointerCount() > 1
+                        if (getActivePointerTrackerCount() > 1 && sPointerTrackerQueue != null
                                 && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
                             onUpEventInternal();
                         }
-                        if (!mIsPossibleGesture) {
+                        if (!mIsDetectingGesture) {
                             mKeyAlreadyProcessed = true;
                         }
                         setReleasedKeyGraphics(oldKey);
@@ -837,7 +856,7 @@
                 if (mIsAllowedSlidingKeyInput) {
                     onMoveToNewKey(key, x, y);
                 } else {
-                    if (!mIsPossibleGesture) {
+                    if (!mIsDetectingGesture) {
                         mKeyAlreadyProcessed = true;
                     }
                 }
@@ -845,13 +864,14 @@
         }
     }
 
-    public void onUpEvent(int x, int y, long eventTime) {
-        if (DEBUG_EVENT)
+    public void onUpEvent(final int x, final int y, final long eventTime) {
+        if (DEBUG_EVENT) {
             printTouchEvent("onUpEvent  :", x, y, eventTime);
+        }
 
         final PointerTrackerQueue queue = sPointerTrackerQueue;
         if (queue != null) {
-            if (!mInGesture) {
+            if (!sInGesture) {
                 if (mCurrentKey != null && mCurrentKey.isModifier()) {
                     // Before processing an up event of modifier key, all pointers already being
                     // tracked should be released.
@@ -860,18 +880,21 @@
                     queue.releaseAllPointersOlderThan(this, eventTime);
                 }
             }
-            queue.remove(this);
         }
         onUpEventInternal();
+        if (queue != null) {
+            queue.remove(this);
+        }
     }
 
     // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
     // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
     // "virtual" up event.
     @Override
-    public void onPhantomUpEvent(long eventTime) {
-        if (DEBUG_EVENT)
+    public void onPhantomUpEvent(final long eventTime) {
+        if (DEBUG_EVENT) {
             printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime);
+        }
         onUpEventInternal();
         mKeyAlreadyProcessed = true;
     }
@@ -879,37 +902,35 @@
     private void onUpEventInternal() {
         mTimerProxy.cancelKeyTimers();
         mIsInSlidingKeyInput = false;
-        mIsPossibleGesture = false;
+        mIsDetectingGesture = false;
+        final Key currentKey = mCurrentKey;
+        mCurrentKey = null;
         // Release the last pressed key.
-        setReleasedKeyGraphics(mCurrentKey);
+        setReleasedKeyGraphics(currentKey);
         if (mIsShowingMoreKeysPanel) {
             mDrawingProxy.dismissMoreKeysPanel();
             mIsShowingMoreKeysPanel = false;
         }
 
-        if (mInGesture) {
-            // Register up event on gesture tracker.
-            // TODO: Figure out how to deal with multiple fingers that are in gesture, sliding,
-            // and/or tapping mode?
-            endBatchInput(getAllBatchPoints());
-            if (mCurrentKey != null) {
-                callListenerOnRelease(mCurrentKey, mCurrentKey.mCode, true);
-                mCurrentKey = null;
+        if (sInGesture) {
+            if (currentKey != null) {
+                callListenerOnRelease(currentKey, currentKey.mCode, true);
             }
-            mDrawingProxy.showGestureTrail(this);
+            endBatchInput();
             return;
         }
-        // This event will be recognized as a regular code input. Clear unused batch points so they
-        // are not mistakenly included in the next batch event.
+        // This event will be recognized as a regular code input. Clear unused possible batch points
+        // so they are not mistakenly displayed as preview.
         clearBatchInputPointsOfAllPointerTrackers();
-        if (mKeyAlreadyProcessed)
+        if (mKeyAlreadyProcessed) {
             return;
-        if (mCurrentKey != null && !mCurrentKey.isRepeatable()) {
-            detectAndSendKey(mCurrentKey, mKeyX, mKeyY);
+        }
+        if (currentKey != null && !currentKey.isRepeatable()) {
+            detectAndSendKey(currentKey, mKeyX, mKeyY);
         }
     }
 
-    public void onShowMoreKeysPanel(int x, int y, KeyEventHandler handler) {
+    public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) {
         abortBatchInput();
         onLongPressed();
         mIsShowingMoreKeysPanel = true;
@@ -925,9 +946,10 @@
         }
     }
 
-    public void onCancelEvent(int x, int y, long eventTime) {
-        if (DEBUG_EVENT)
+    public void onCancelEvent(final int x, final int y, final long eventTime) {
+        if (DEBUG_EVENT) {
             printTouchEvent("onCancelEvt:", x, y, eventTime);
+        }
 
         final PointerTrackerQueue queue = sPointerTrackerQueue;
         if (queue != null) {
@@ -947,24 +969,25 @@
         }
     }
 
-    private void startRepeatKey(Key key) {
-        if (key != null && key.isRepeatable() && !mInGesture) {
+    private void startRepeatKey(final Key key) {
+        if (key != null && key.isRepeatable() && !sInGesture) {
             onRegisterKey(key);
             mTimerProxy.startKeyRepeatTimer(this);
         }
     }
 
-    public void onRegisterKey(Key key) {
+    public void onRegisterKey(final Key key) {
         if (key != null) {
             detectAndSendKey(key, key.mX, key.mY);
             mTimerProxy.startTypingStateTimer(key);
         }
     }
 
-    private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, Key newKey) {
-        if (mKeyDetector == null)
+    private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final Key newKey) {
+        if (mKeyDetector == null) {
             throw new NullPointerException("keyboard and/or key detector not set");
-        Key curKey = mCurrentKey;
+        }
+        final Key curKey = mCurrentKey;
         if (newKey == curKey) {
             return false;
         } else if (curKey != null) {
@@ -975,25 +998,25 @@
         }
     }
 
-    private void startLongPressTimer(Key key) {
-        if (key != null && key.isLongPressEnabled() && !mInGesture) {
+    private void startLongPressTimer(final Key key) {
+        if (key != null && key.isLongPressEnabled() && !sInGesture) {
             mTimerProxy.startLongPressTimer(this);
         }
     }
 
-    private void detectAndSendKey(Key key, int x, int y) {
+    private void detectAndSendKey(final Key key, final int x, final int y) {
         if (key == null) {
             callListenerOnCancelInput();
             return;
         }
 
-        int code = key.mCode;
+        final int code = key.mCode;
         callListenerOnCodeInput(key, code, x, y);
         callListenerOnRelease(key, code, false);
-        sWasInGesture = false;
     }
 
-    private void printTouchEvent(String title, int x, int y, long eventTime) {
+    private void printTouchEvent(final String title, final int x, final int y,
+            final long eventTime) {
         final Key key = mKeyDetector.detectHitKey(x, y);
         final String code = KeyDetector.printableCode(key);
         Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title,
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index ae123e2..71bf31f 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -18,9 +18,9 @@
 
 import android.graphics.Rect;
 import android.text.TextUtils;
-import android.util.FloatMath;
 
 import com.android.inputmethod.keyboard.Keyboard.Params.TouchPositionCorrection;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.JniUtils;
 
 import java.util.Arrays;
@@ -112,7 +112,7 @@
         final Key[] keys = mKeys;
         final TouchPositionCorrection touchPositionCorrection = mTouchPositionCorrection;
         final int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE];
-        Arrays.fill(proximityCharsArray, KeyDetector.NOT_A_CODE);
+        Arrays.fill(proximityCharsArray, Constants.NOT_A_CODE);
         for (int i = 0; i < mGridSize; ++i) {
             final int proximityCharsLength = gridNeighborKeys[i].length;
             for (int j = 0; j < proximityCharsLength; ++j) {
@@ -155,7 +155,9 @@
                     final float radius = touchPositionCorrection.mRadii[row];
                     sweetSpotCenterXs[i] = hitBox.exactCenterX() + x * hitBoxWidth;
                     sweetSpotCenterYs[i] = hitBox.exactCenterY() + y * hitBoxHeight;
-                    sweetSpotRadii[i] = radius * FloatMath.sqrt(
+                    // Note that, in recent versions of Android, FloatMath is actually slower than
+                    // java.lang.Math due to the way the JIT optimizes java.lang.Math.
+                    sweetSpotRadii[i] = radius * (float)Math.sqrt(
                             hitBoxWidth * hitBoxWidth + hitBoxHeight * hitBoxHeight);
                 }
             }
@@ -233,7 +235,7 @@
             dest[index++] = code;
         }
         if (index < destLength) {
-            dest[index] = KeyDetector.NOT_A_CODE;
+            dest[index] = Constants.NOT_A_CODE;
         }
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
new file mode 100644
index 0000000..e814d80
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
@@ -0,0 +1,166 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.SystemClock;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResizableIntArray;
+
+class GesturePreviewTrail {
+    private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewTrail.PREVIEW_CAPACITY;
+
+    private final GesturePreviewTrailParams mPreviewParams;
+    private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+    private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+    private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
+    private int mCurrentStrokeId = -1;
+    private long mCurrentDownTime;
+    private int mTrailStartIndex;
+
+    // Use this value as imaginary zero because x-coordinates may be zero.
+    private static final int DOWN_EVENT_MARKER = -128;
+
+    static class GesturePreviewTrailParams {
+        public final int mFadeoutStartDelay;
+        public final int mFadeoutDuration;
+        public final int mUpdateInterval;
+
+        public GesturePreviewTrailParams(final TypedArray keyboardViewAttr) {
+            mFadeoutStartDelay = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
+            mFadeoutDuration = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0);
+            mUpdateInterval = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0);
+        }
+    }
+
+    public GesturePreviewTrail(final GesturePreviewTrailParams params) {
+        mPreviewParams = params;
+    }
+
+    private static int markAsDownEvent(final int xCoord) {
+        return DOWN_EVENT_MARKER - xCoord;
+    }
+
+    private static boolean isDownEventXCoord(final int xCoordOrMark) {
+        return xCoordOrMark <= DOWN_EVENT_MARKER;
+    }
+
+    private static int getXCoordValue(final int xCoordOrMark) {
+        return isDownEventXCoord(xCoordOrMark)
+                ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark;
+    }
+
+    public void addStroke(final GestureStrokeWithPreviewTrail stroke, final long downTime) {
+        final int strokeId = stroke.getGestureStrokeId();
+        final boolean isNewStroke = strokeId != mCurrentStrokeId;
+        final int trailSize = mEventTimes.getLength();
+        stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
+        final int newTrailSize = mEventTimes.getLength();
+        if (stroke.getGestureStrokePreviewSize() == 0) {
+            return;
+        }
+        if (isNewStroke) {
+            final int elapsedTime = (int)(downTime - mCurrentDownTime);
+            final int[] eventTimes = mEventTimes.getPrimitiveArray();
+            for (int i = mTrailStartIndex; i < trailSize; i++) {
+                eventTimes[i] -= elapsedTime;
+            }
+
+            if (newTrailSize > trailSize) {
+                final int[] xCoords = mXCoordinates.getPrimitiveArray();
+                xCoords[trailSize] = markAsDownEvent(xCoords[trailSize]);
+            }
+            mCurrentDownTime = downTime;
+            mCurrentStrokeId = strokeId;
+        }
+    }
+
+    private int getAlpha(final int elapsedTime) {
+        if (elapsedTime < mPreviewParams.mFadeoutStartDelay) {
+            return Constants.Color.ALPHA_OPAQUE;
+        }
+        final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE
+                * (elapsedTime - mPreviewParams.mFadeoutStartDelay)
+                / mPreviewParams.mFadeoutDuration;
+        return Constants.Color.ALPHA_OPAQUE - decreasingAlpha;
+    }
+
+    /**
+     * Draw gesture preview trail
+     * @param canvas The canvas to draw the gesture preview trail
+     * @param paint The paint object to be used to draw the gesture preview trail
+     * @return true if some gesture preview trails remain to be drawn
+     */
+    public boolean drawGestureTrail(final Canvas canvas, final Paint paint) {
+        final int trailSize = mEventTimes.getLength();
+        if (trailSize == 0) {
+            return false;
+        }
+
+        final int[] eventTimes = mEventTimes.getPrimitiveArray();
+        final int[] xCoords = mXCoordinates.getPrimitiveArray();
+        final int[] yCoords = mYCoordinates.getPrimitiveArray();
+        final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentDownTime);
+        final int lingeringDuration = mPreviewParams.mFadeoutStartDelay
+                + mPreviewParams.mFadeoutDuration;
+        int startIndex;
+        for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) {
+            final int elapsedTime = sinceDown - eventTimes[startIndex];
+            // Skip too old trail points.
+            if (elapsedTime < lingeringDuration) {
+                break;
+            }
+        }
+        mTrailStartIndex = startIndex;
+
+        if (startIndex < trailSize) {
+            int lastX = getXCoordValue(xCoords[startIndex]);
+            int lastY = yCoords[startIndex];
+            for (int i = startIndex + 1; i < trailSize - 1; i++) {
+                final int x = xCoords[i];
+                final int y = yCoords[i];
+                final int elapsedTime = sinceDown - eventTimes[i];
+                // Draw trail line only when the current point isn't a down point.
+                if (!isDownEventXCoord(x)) {
+                    paint.setAlpha(getAlpha(elapsedTime));
+                    canvas.drawLine(lastX, lastY, x, y, paint);
+                }
+                lastX = getXCoordValue(x);
+                lastY = y;
+            }
+        }
+
+        final int newSize = trailSize - startIndex;
+        if (newSize < startIndex) {
+            mTrailStartIndex = 0;
+            if (newSize > 0) {
+                System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
+                System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
+                System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
+            }
+            mEventTimes.setLength(newSize);
+            mXCoordinates.setLength(newSize);
+            mYCoordinates.setLength(newSize);
+        }
+        return newSize > 0;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
index 28d6c1d..8251344 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -14,11 +14,6 @@
 
 package com.android.inputmethod.keyboard.internal;
 
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.util.FloatMath;
-
-import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.InputPointers;
 import com.android.inputmethod.latin.ResizableIntArray;
 
@@ -38,44 +33,30 @@
     private int mLastPointY;
 
     private int mMinGestureLength;
-    private int mMinGestureLengthWhileInGesture;
     private int mMinGestureSampleLength;
 
     // TODO: Move some of these to resource.
-    private static final float MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH = 1.0f;
-    private static final float MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH_WHILE_IN_GESTURE = 0.5f;
-    private static final int MIN_GESTURE_DURATION = 150; // msec
-    private static final int MIN_GESTURE_DURATION_WHILE_IN_GESTURE = 75; // msec
-    private static final float MIN_GESTURE_SAMPLING_RATIO_TO_KEY_HEIGHT = 1.0f / 6.0f;
+    private static final float MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH = 0.75f;
+    private static final int MIN_GESTURE_DURATION = 100; // msec
+    private static final float MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH = 1.0f / 6.0f;
     private static final float GESTURE_RECOG_SPEED_THRESHOLD = 0.4f; // dip/msec
     private static final float GESTURE_RECOG_CURVATURE_THRESHOLD = (float)(Math.PI / 4.0f);
 
-    private static final float DOUBLE_PI = (float)(2 * Math.PI);
+    private static final float DOUBLE_PI = (float)(2.0f * Math.PI);
 
-    // Fade based on number of gesture samples, see MIN_GESTURE_SAMPLING_RATIO_TO_KEY_HEIGHT
-    private static final int DRAWING_GESTURE_FADE_START = 10;
-    private static final int DRAWING_GESTURE_FADE_RATE = 6;
-
-    public GestureStroke(int pointerId) {
+    public GestureStroke(final int pointerId) {
         mPointerId = pointerId;
-        reset();
     }
 
-    public void setGestureSampleLength(final int keyWidth, final int keyHeight) {
+    public void setGestureSampleLength(final int keyWidth) {
         // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
         mMinGestureLength = (int)(keyWidth * MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH);
-        mMinGestureLengthWhileInGesture = (int)(
-                keyWidth * MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH_WHILE_IN_GESTURE);
-        mMinGestureSampleLength = (int)(keyHeight * MIN_GESTURE_SAMPLING_RATIO_TO_KEY_HEIGHT);
+        mMinGestureSampleLength = (int)(keyWidth * MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH);
     }
 
-    public boolean isStartOfAGesture(final int downDuration, final boolean wasInGesture) {
-        // The tolerance of the time duration and the stroke length to detect the start of a
-        // gesture stroke should be eased when the previous input was a gesture input.
-        if (wasInGesture) {
-            return downDuration > MIN_GESTURE_DURATION_WHILE_IN_GESTURE
-                    && mLength > mMinGestureLengthWhileInGesture;
-        }
+    public boolean isStartOfAGesture() {
+        final int size = mEventTimes.getLength();
+        final int downDuration = (size > 0) ? mEventTimes.get(size - 1) : 0;
         return downDuration > MIN_GESTURE_DURATION && mLength > mMinGestureLength;
     }
 
@@ -149,23 +130,29 @@
     }
 
     private void appendBatchPoints(final InputPointers out, final int size) {
+        final int length = size - mLastIncrementalBatchSize;
+        if (length <= 0) {
+            return;
+        }
         out.append(mPointerId, mEventTimes, mXCoordinates, mYCoordinates,
-                mLastIncrementalBatchSize, size - mLastIncrementalBatchSize);
+                mLastIncrementalBatchSize, length);
         mLastIncrementalBatchSize = size;
     }
 
-    private static float getDistance(final int p1x, final int p1y,
-            final int p2x, final int p2y) {
-        final float dx = p1x - p2x;
-        final float dy = p1y - p2y;
-        // TODO: Optimize out this {@link FloatMath#sqrt(float)} call.
-        return FloatMath.sqrt(dx * dx + dy * dy);
+    private static float getDistance(final int x1, final int y1, final int x2, final int y2) {
+        final float dx = x1 - x2;
+        final float dy = y1 - y2;
+        // Note that, in recent versions of Android, FloatMath is actually slower than
+        // java.lang.Math due to the way the JIT optimizes java.lang.Math.
+        return (float)Math.sqrt(dx * dx + dy * dy);
     }
 
-    private static float getAngle(final int p1x, final int p1y, final int p2x, final int p2y) {
-        final int dx = p1x - p2x;
-        final int dy = p1y - p2y;
+    private static float getAngle(final int x1, final int y1, final int x2, final int y2) {
+        final int dx = x1 - x2;
+        final int dy = y1 - y2;
         if (dx == 0 && dy == 0) return 0;
+        // Would it be faster to call atan2f() directly via JNI?  Not sure about what the JIT
+        // does with Math.atan2().
         return (float)Math.atan2(dy, dx);
     }
 
@@ -176,23 +163,4 @@
         }
         return diff;
     }
-
-    public void drawGestureTrail(Canvas canvas, Paint paint, int lastX, int lastY) {
-        // TODO: These paint parameter interpolation should be tunable, possibly introduce an object
-        // that implements an interface such as Paint getPaint(int step, int strokePoints)
-        final int size = mXCoordinates.getLength();
-        int[] xCoords = mXCoordinates.getPrimitiveArray();
-        int[] yCoords = mYCoordinates.getPrimitiveArray();
-        int alpha = Constants.Color.ALPHA_OPAQUE;
-        for (int i = size - 1; i > 0 && alpha > 0; i--) {
-            paint.setAlpha(alpha);
-            if (size - i > DRAWING_GESTURE_FADE_START) {
-                alpha -= DRAWING_GESTURE_FADE_RATE;
-            }
-            canvas.drawLine(xCoords[i - 1], yCoords[i - 1], xCoords[i], yCoords[i], paint);
-            if (i == size - 1) {
-                canvas.drawLine(lastX, lastY, xCoords[i], yCoords[i], paint);
-            }
-        }
-    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java
new file mode 100644
index 0000000..6c1a9bc
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java
@@ -0,0 +1,70 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.latin.ResizableIntArray;
+
+public class GestureStrokeWithPreviewTrail extends GestureStroke {
+    public static final int PREVIEW_CAPACITY = 256;
+
+    private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY);
+    private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
+    private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
+
+    private int mStrokeId;
+    private int mLastPreviewSize;
+
+    public GestureStrokeWithPreviewTrail(final int pointerId) {
+        super(pointerId);
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        mStrokeId++;
+        mLastPreviewSize = 0;
+        mPreviewEventTimes.setLength(0);
+        mPreviewXCoordinates.setLength(0);
+        mPreviewYCoordinates.setLength(0);
+    }
+
+    public int getGestureStrokeId() {
+        return mStrokeId;
+    }
+
+    public int getGestureStrokePreviewSize() {
+        return mPreviewEventTimes.getLength();
+    }
+
+    @Override
+    public void addPoint(final int x, final int y, final int time, final boolean isHistorical) {
+        super.addPoint(x, y, time, isHistorical);
+        mPreviewEventTimes.add(time);
+        mPreviewXCoordinates.add(x);
+        mPreviewYCoordinates.add(y);
+    }
+
+    public void appendPreviewStroke(final ResizableIntArray eventTimes,
+            final ResizableIntArray xCoords, final ResizableIntArray yCoords) {
+        final int length = mPreviewEventTimes.getLength() - mLastPreviewSize;
+        if (length <= 0) {
+            return;
+        }
+        eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length);
+        xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length);
+        yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
+        mLastPreviewSize = mPreviewEventTimes.getLength();
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index 94a7b82..13214bb 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.StringUtils;
 
@@ -258,7 +259,7 @@
             throw new IllegalArgumentException();
         }
 
-        final ArrayList<T> list = new ArrayList<T>(end - start);
+        final ArrayList<T> list = CollectionUtils.newArrayList(end - start);
         for (int i = start; i < end; i++) {
             list.add(array[i]);
         }
@@ -438,7 +439,7 @@
                 // Skip empty entry.
                 if (pos - start > 0) {
                     if (list == null) {
-                        list = new ArrayList<String>();
+                        list = CollectionUtils.newArrayList();
                     }
                     list.add(text.substring(start, pos));
                 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
index 291b3b9..e40cf45 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
@@ -21,6 +21,7 @@
 import android.util.SparseArray;
 
 import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.XmlParseUtils;
 
@@ -33,7 +34,7 @@
     private static final String TAG = KeyStyles.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    final HashMap<String, KeyStyle> mStyles = new HashMap<String, KeyStyle>();
+    final HashMap<String, KeyStyle> mStyles = CollectionUtils.newHashMap();
 
     final KeyboardTextsSet mTextsSet;
     private final KeyStyle mEmptyKeyStyle;
@@ -90,7 +91,7 @@
 
     private class DeclaredKeyStyle extends KeyStyle {
         private final String mParentStyleName;
-        private final SparseArray<Object> mStyleAttributes = new SparseArray<Object>();
+        private final SparseArray<Object> mStyleAttributes = CollectionUtils.newSparseArray();
 
         public DeclaredKeyStyle(String parentStyleName) {
             mParentStyleName = parentStyleName;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index f7981a3..f7923d0 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -17,13 +17,13 @@
 package com.android.inputmethod.keyboard.internal;
 
 import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.CollectionUtils;
 
 import java.util.HashMap;
 
 public class KeyboardCodesSet {
-    private static final HashMap<String, int[]> sLanguageToCodesMap =
-            new HashMap<String, int[]>();
-    private static final HashMap<String, Integer> sNameToIdMap = new HashMap<String, Integer>();
+    private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap();
+    private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap();
 
     private int[] mCodes = DEFAULT;
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 5155851..4a98a36 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -22,6 +22,7 @@
 import android.util.Log;
 import android.util.SparseIntArray;
 
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 
 import java.util.HashMap;
@@ -35,7 +36,7 @@
     private static final SparseIntArray ATTR_ID_TO_ICON_ID = new SparseIntArray();
 
     // Icon name to icon id map.
-    private static final HashMap<String, Integer> sNameToIdsMap = new HashMap<String, Integer>();
+    private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
 
     private static final Object[] NAMES_AND_ATTR_IDS = {
         "undefined",                    ATTR_UNDEFINED,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index bec0f1f..a608cde 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 
 import java.util.HashMap;
@@ -45,14 +46,12 @@
  */
 public final class KeyboardTextsSet {
     // Language to texts map.
-    private static final HashMap<String, String[]> sLocaleToTextsMap =
-            new HashMap<String, String[]>();
-    private static final HashMap<String, Integer> sNameToIdsMap =
-            new HashMap<String, Integer>();
+    private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap();
+    private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
 
     private String[] mTexts;
     // Resource name to text map.
-    private HashMap<String, String> mResourceNameToTextsMap = new HashMap<String, String>();
+    private HashMap<String, String> mResourceNameToTextsMap = CollectionUtils.newHashMap();
 
     public void setLanguage(final String language) {
         mTexts = sLocaleToTextsMap.get(language);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index bd16480..e0858c0 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -18,85 +18,148 @@
 
 import android.util.Log;
 
-import java.util.Iterator;
-import java.util.LinkedList;
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.ArrayList;
 
 public class PointerTrackerQueue {
     private static final String TAG = PointerTrackerQueue.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    public interface ElementActions {
+    public interface Element {
         public boolean isModifier();
         public boolean isInSlidingKeyInput();
         public void onPhantomUpEvent(long eventTime);
     }
 
-    // TODO: Use ring buffer instead of {@link LinkedList}.
-    private final LinkedList<ElementActions> mQueue = new LinkedList<ElementActions>();
+    private static final int INITIAL_CAPACITY = 10;
+    private final ArrayList<Element> mExpandableArrayOfActivePointers =
+            CollectionUtils.newArrayList(INITIAL_CAPACITY);
+    private int mArraySize = 0;
 
-    public int size() {
-        return mQueue.size();
+    public synchronized int size() {
+        return mArraySize;
     }
 
-    public synchronized void add(ElementActions tracker) {
-        mQueue.add(tracker);
+    public synchronized void add(final Element pointer) {
+        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+        final int arraySize = mArraySize;
+        if (arraySize < expandableArray.size()) {
+            expandableArray.set(arraySize, pointer);
+        } else {
+            expandableArray.add(pointer);
+        }
+        mArraySize = arraySize + 1;
     }
 
-    public synchronized void remove(ElementActions tracker) {
-        mQueue.remove(tracker);
+    public synchronized void remove(final Element pointer) {
+        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+        final int arraySize = mArraySize;
+        int newSize = 0;
+        for (int index = 0; index < arraySize; index++) {
+            final Element element = expandableArray.get(index);
+            if (element == pointer) {
+                if (newSize != index) {
+                    Log.w(TAG, "Found duplicated element in remove: " + pointer);
+                }
+                continue; // Remove this element from the expandableArray.
+            }
+            if (newSize != index) {
+                // Shift this element toward the beginning of the expandableArray.
+                expandableArray.set(newSize, element);
+            }
+            newSize++;
+        }
+        mArraySize = newSize;
     }
 
-    public synchronized void releaseAllPointersOlderThan(ElementActions tracker,
-            long eventTime) {
+    public synchronized void releaseAllPointersOlderThan(final Element pointer,
+            final long eventTime) {
         if (DEBUG) {
-            Log.d(TAG, "releaseAllPoniterOlderThan: " + tracker + " " + this);
+            Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this);
         }
-        if (!mQueue.contains(tracker)) {
-            return;
-        }
-        final Iterator<ElementActions> it = mQueue.iterator();
-        while (it.hasNext()) {
-            final ElementActions t = it.next();
-            if (t == tracker) {
-                break;
+        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+        final int arraySize = mArraySize;
+        int newSize, index;
+        for (newSize = index = 0; index < arraySize; index++) {
+            final Element element = expandableArray.get(index);
+            if (element == pointer) {
+                break; // Stop releasing elements.
             }
-            if (!t.isModifier()) {
-                t.onPhantomUpEvent(eventTime);
-                it.remove();
+            if (!element.isModifier()) {
+                element.onPhantomUpEvent(eventTime);
+                continue; // Remove this element from the expandableArray.
+            }
+            if (newSize != index) {
+                // Shift this element toward the beginning of the expandableArray.
+                expandableArray.set(newSize, element);
+            }
+            newSize++;
+        }
+        // Shift rest of the expandableArray.
+        int count = 0;
+        for (; index < arraySize; index++) {
+            final Element element = expandableArray.get(index);
+            if (element == pointer) {
+                if (count > 0) {
+                    Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: "
+                            + pointer);
+                }
+                count++;
+            }
+            if (newSize != index) {
+                expandableArray.set(newSize, expandableArray.get(index));
+                newSize++;
             }
         }
+        mArraySize = newSize;
     }
 
-    public void releaseAllPointers(long eventTime) {
+    public void releaseAllPointers(final long eventTime) {
         releaseAllPointersExcept(null, eventTime);
     }
 
-    public synchronized void releaseAllPointersExcept(ElementActions tracker, long eventTime) {
+    public synchronized void releaseAllPointersExcept(final Element pointer,
+            final long eventTime) {
         if (DEBUG) {
-            if (tracker == null) {
+            if (pointer == null) {
                 Log.d(TAG, "releaseAllPoniters: " + this);
             } else {
-                Log.d(TAG, "releaseAllPoniterExcept: " + tracker + " " + this);
+                Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this);
             }
         }
-        final Iterator<ElementActions> it = mQueue.iterator();
-        while (it.hasNext()) {
-            final ElementActions t = it.next();
-            if (t != tracker) {
-                t.onPhantomUpEvent(eventTime);
-                it.remove();
+        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+        final int arraySize = mArraySize;
+        int newSize = 0, count = 0;
+        for (int index = 0; index < arraySize; index++) {
+            final Element element = expandableArray.get(index);
+            if (element == pointer) {
+                if (count > 0) {
+                    Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: " + pointer);
+                }
+                count++;
+            } else {
+                element.onPhantomUpEvent(eventTime);
+                continue; // Remove this element from the expandableArray.
             }
+            if (newSize != index) {
+                // Shift this element toward the beginning of the expandableArray.
+                expandableArray.set(newSize, element);
+            }
+            newSize++;
         }
+        mArraySize = newSize;
     }
 
-    public synchronized boolean hasModifierKeyOlderThan(ElementActions tracker) {
-        final Iterator<ElementActions> it = mQueue.iterator();
-        while (it.hasNext()) {
-            final ElementActions t = it.next();
-            if (t == tracker) {
-                break;
+    public synchronized boolean hasModifierKeyOlderThan(final Element pointer) {
+        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+        final int arraySize = mArraySize;
+        for (int index = 0; index < arraySize; index++) {
+            final Element element = expandableArray.get(index);
+            if (element == pointer) {
+                return false; // Stop searching modifier key.
             }
-            if (t.isModifier()) {
+            if (element.isModifier()) {
                 return true;
             }
         }
@@ -104,8 +167,11 @@
     }
 
     public synchronized boolean isAnyInSlidingKeyInput() {
-        for (final ElementActions tracker : mQueue) {
-            if (tracker.isInSlidingKeyInput()) {
+        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+        final int arraySize = mArraySize;
+        for (int index = 0; index < arraySize; index++) {
+            final Element element = expandableArray.get(index);
+            if (element.isInSlidingKeyInput()) {
                 return true;
             }
         }
@@ -113,12 +179,15 @@
     }
 
     @Override
-    public String toString() {
+    public synchronized String toString() {
         final StringBuilder sb = new StringBuilder();
-        for (final ElementActions tracker : mQueue) {
+        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+        final int arraySize = mArraySize;
+        for (int index = 0; index < arraySize; index++) {
+            final Element element = expandableArray.get(index);
             if (sb.length() > 0)
                 sb.append(" ");
-            sb.append(tracker.toString());
+            sb.append(element.toString());
         }
         return "[" + sb.toString() + "]";
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
index d0fecf0..269b202 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
@@ -23,10 +23,13 @@
 import android.graphics.Paint.Align;
 import android.os.Message;
 import android.text.TextUtils;
+import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.widget.RelativeLayout;
 
 import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.GesturePreviewTrailParams;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 
@@ -46,29 +49,42 @@
     private int mXOrigin;
     private int mYOrigin;
 
-    private final SparseArray<PointerTracker> mPointers = new SparseArray<PointerTracker>();
+    private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails =
+            CollectionUtils.newSparseArray();
+    private final GesturePreviewTrailParams mGesturePreviewTrailParams;
 
     private String mGestureFloatingPreviewText;
+    private int mLastPointerX;
+    private int mLastPointerY;
+
     private boolean mDrawsGesturePreviewTrail;
     private boolean mDrawsGestureFloatingPreviewText;
 
-    private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
+    private final DrawingHandler mDrawingHandler;
 
     private static class DrawingHandler extends StaticInnerHandlerWrapper<PreviewPlacerView> {
         private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 0;
+        private static final int MSG_UPDATE_GESTURE_PREVIEW_TRAIL = 1;
 
-        public DrawingHandler(PreviewPlacerView outerInstance) {
+        private final GesturePreviewTrailParams mGesturePreviewTrailParams;
+
+        public DrawingHandler(final PreviewPlacerView outerInstance,
+                final GesturePreviewTrailParams gesturePreviewTrailParams) {
             super(outerInstance);
+            mGesturePreviewTrailParams = gesturePreviewTrailParams;
         }
 
         @Override
-        public void handleMessage(Message msg) {
+        public void handleMessage(final Message msg) {
             final PreviewPlacerView placerView = getOuterInstance();
             if (placerView == null) return;
             switch (msg.what) {
             case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
                 placerView.setGestureFloatingPreviewText(null);
                 break;
+            case MSG_UPDATE_GESTURE_PREVIEW_TRAIL:
+                placerView.invalidate();
+                break;
             }
         }
 
@@ -84,15 +100,32 @@
                     placerView.mGestureFloatingPreviewTextLingerTimeout);
         }
 
+        private void cancelUpdateGestureTrailPreview() {
+            removeMessages(MSG_UPDATE_GESTURE_PREVIEW_TRAIL);
+        }
+
+        public void postUpdateGestureTrailPreview() {
+            cancelUpdateGestureTrailPreview();
+            sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_TRAIL),
+                    mGesturePreviewTrailParams.mUpdateInterval);
+        }
+
         public void cancelAllMessages() {
             cancelDismissGestureFloatingPreviewText();
+            cancelUpdateGestureTrailPreview();
         }
     }
 
-    public PreviewPlacerView(Context context, TypedArray keyboardViewAttr) {
+    public PreviewPlacerView(final Context context, final AttributeSet attrs) {
+        this(context, attrs, R.attr.keyboardViewStyle);
+    }
+
+    public PreviewPlacerView(final Context context, final AttributeSet attrs, final int defStyle) {
         super(context);
         setWillNotDraw(false);
 
+        final TypedArray keyboardViewAttr = context.obtainStyledAttributes(
+                attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
         final int gestureFloatingPreviewTextSize = keyboardViewAttr.getDimensionPixelSize(
                 R.styleable.KeyboardView_gestureFloatingPreviewTextSize, 0);
         mGestureFloatingPreviewTextColor = keyboardViewAttr.getColor(
@@ -117,6 +150,10 @@
                 R.styleable.KeyboardView_gesturePreviewTrailColor, 0);
         final int gesturePreviewTrailWidth = keyboardViewAttr.getDimensionPixelSize(
                 R.styleable.KeyboardView_gesturePreviewTrailWidth, 0);
+        mGesturePreviewTrailParams = new GesturePreviewTrailParams(keyboardViewAttr);
+        keyboardViewAttr.recycle();
+
+        mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams);
 
         mGesturePaint = new Paint();
         mGesturePaint.setAntiAlias(true);
@@ -132,48 +169,60 @@
         mTextPaint.setTextSize(gestureFloatingPreviewTextSize);
     }
 
-    public void setOrigin(int x, int y) {
+    public void setOrigin(final int x, final int y) {
         mXOrigin = x;
         mYOrigin = y;
     }
 
-    public void setGesturePreviewMode(boolean drawsGesturePreviewTrail,
-            boolean drawsGestureFloatingPreviewText) {
+    public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+            final boolean drawsGestureFloatingPreviewText) {
         mDrawsGesturePreviewTrail = drawsGesturePreviewTrail;
         mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText;
     }
 
-    public void invalidatePointer(PointerTracker tracker) {
-        synchronized (mPointers) {
-            mPointers.put(tracker.mPointerId, tracker);
-            // TODO: Should narrow the invalidate region.
-            invalidate();
+    public void invalidatePointer(final PointerTracker tracker) {
+        GesturePreviewTrail trail;
+        synchronized (mGesturePreviewTrails) {
+            trail = mGesturePreviewTrails.get(tracker.mPointerId);
+            if (trail == null) {
+                trail = new GesturePreviewTrail(mGesturePreviewTrailParams);
+                mGesturePreviewTrails.put(tracker.mPointerId, trail);
+            }
         }
+        trail.addStroke(tracker.getGestureStrokeWithPreviewTrail(), tracker.getDownTime());
+
+        mLastPointerX = tracker.getLastX();
+        mLastPointerY = tracker.getLastY();
+        // TODO: Should narrow the invalidate region.
+        invalidate();
     }
 
     @Override
-    public void onDraw(Canvas canvas) {
+    public void onDraw(final Canvas canvas) {
         super.onDraw(canvas);
-        synchronized (mPointers) {
-            canvas.translate(mXOrigin, mYOrigin);
-            final int trackerCount = mPointers.size();
-            boolean hasDrawnFloatingPreviewText = false;
-            for (int index = 0; index < trackerCount; index++) {
-                final PointerTracker tracker = mPointers.valueAt(index);
-                if (mDrawsGesturePreviewTrail) {
-                    tracker.drawGestureTrail(canvas, mGesturePaint);
-                }
-                // TODO: Figure out more cleaner way to draw gesture preview text.
-                if (mDrawsGestureFloatingPreviewText && !hasDrawnFloatingPreviewText) {
-                    drawGestureFloatingPreviewText(canvas, tracker, mGestureFloatingPreviewText);
-                    hasDrawnFloatingPreviewText = true;
+        canvas.translate(mXOrigin, mYOrigin);
+        if (mDrawsGesturePreviewTrail) {
+            boolean needsUpdatingGesturePreviewTrail = false;
+            synchronized (mGesturePreviewTrails) {
+                // Trails count == fingers count that have ever been active.
+                final int trailsCount = mGesturePreviewTrails.size();
+                for (int index = 0; index < trailsCount; index++) {
+                    final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index);
+                    needsUpdatingGesturePreviewTrail |=
+                            trail.drawGestureTrail(canvas, mGesturePaint);
                 }
             }
-            canvas.translate(-mXOrigin, -mYOrigin);
+            if (needsUpdatingGesturePreviewTrail) {
+                mDrawingHandler.postUpdateGestureTrailPreview();
+            }
         }
+        if (mDrawsGestureFloatingPreviewText) {
+            drawGestureFloatingPreviewText(canvas, mGestureFloatingPreviewText);
+        }
+        canvas.translate(-mXOrigin, -mYOrigin);
     }
 
-    public void setGestureFloatingPreviewText(String gestureFloatingPreviewText) {
+    public void setGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
         mGestureFloatingPreviewText = gestureFloatingPreviewText;
         invalidate();
     }
@@ -186,15 +235,17 @@
         mDrawingHandler.cancelAllMessages();
     }
 
-    private void drawGestureFloatingPreviewText(Canvas canvas, PointerTracker tracker,
-            String gestureFloatingPreviewText) {
+    private void drawGestureFloatingPreviewText(final Canvas canvas,
+            final String gestureFloatingPreviewText) {
         if (TextUtils.isEmpty(gestureFloatingPreviewText)) {
             return;
         }
 
         final Paint paint = mTextPaint;
-        final int lastX = tracker.getLastX();
-        final int lastY = tracker.getLastY();
+        // TODO: Figure out how we should deal with the floating preview text with multiple moving
+        // fingers.
+        final int lastX = mLastPointerX;
+        final int lastY = mLastPointerY;
         final int textSize = (int)paint.getTextSize();
         final int canvasWidth = canvas.getWidth();
 
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
index f8f1395..4b47a26 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
@@ -91,7 +91,7 @@
         }
         final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR);
         final ArrayList<InputMethodSubtype> subtypesList =
-                new ArrayList<InputMethodSubtype>(prefSubtypeArray.length);
+                CollectionUtils.newArrayList(prefSubtypeArray.length);
         for (final String prefSubtype : prefSubtypeArray) {
             final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype);
             if (subtype.getNameResId() == SubtypeLocale.UNKNOWN_KEYBOARD_LAYOUT) {
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
index 779a388..d01592a 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
@@ -89,7 +89,7 @@
             super(context, android.R.layout.simple_spinner_item);
             setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 
-            final TreeSet<SubtypeLocaleItem> items = new TreeSet<SubtypeLocaleItem>();
+            final TreeSet<SubtypeLocaleItem> items = CollectionUtils.newTreeSet();
             final InputMethodInfo imi = ImfUtils.getInputMethodInfoOfThisIme(context);
             final int count = imi.getSubtypeCount();
             for (int i = 0; i < count; i++) {
@@ -533,7 +533,7 @@
 
     private InputMethodSubtype[] getSubtypes() {
         final PreferenceGroup group = getPreferenceScreen();
-        final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+        final ArrayList<InputMethodSubtype> subtypes = CollectionUtils.newArrayList();
         final int count = group.getPreferenceCount();
         for (int i = 0; i < count; i++) {
             final Preference pref = group.getPreference(i);
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index a663374..01ba300 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -39,7 +39,6 @@
         }
         final CharSequence lowerCasedWord = word.toString().toLowerCase();
         for (final String key : dictionaries.keySet()) {
-            if (key.equals(Dictionary.TYPE_WHITELIST)) continue;
             final Dictionary dictionary = dictionaries.get(key);
             // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
             // managing to get null in here. Presumably the language is changing to a language with
@@ -64,7 +63,6 @@
         }
         int maxFreq = -1;
         for (final String key : dictionaries.keySet()) {
-            if (key.equals(Dictionary.TYPE_WHITELIST)) continue;
             final Dictionary dictionary = dictionaries.get(key);
             if (null == dictionary) continue;
             final int tempFreq = dictionary.getFrequency(word);
@@ -75,17 +73,10 @@
         return maxFreq;
     }
 
-    // Returns true if this is a whitelist entry, or it isn't in any dictionary.
-    public static boolean isWhitelistedOrNotAWord(
+    // Returns true if this isn't in any dictionary.
+    public static boolean isNotAWord(
             final ConcurrentHashMap<String, Dictionary> dictionaries,
             final CharSequence word, final boolean ignoreCase) {
-        final WhitelistDictionary whitelistDictionary =
-                (WhitelistDictionary)dictionaries.get(Dictionary.TYPE_WHITELIST);
-        // If "word" is in the whitelist dictionary, it should not be auto corrected.
-        if (whitelistDictionary != null
-                && whitelistDictionary.shouldForciblyAutoCorrectFrom(word)) {
-            return true;
-        }
         return !isValidWord(dictionaries, word, ignoreCase);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 534cffb..8909526 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.text.TextUtils;
+import android.util.SparseArray;
 
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -51,7 +52,9 @@
     private static final int TYPED_LETTER_MULTIPLIER = 2;
 
     private long mNativeDict;
-    private final int[] mInputCodes = new int[MAX_WORD_LENGTH];
+    private final Locale mLocale;
+    private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH];
+    // TODO: The below should be int[] mOutputCodePoints
     private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_RESULTS];
     private final int[] mSpaceIndices = new int[MAX_SPACES];
     private final int[] mOutputScores = new int[MAX_RESULTS];
@@ -59,6 +62,25 @@
 
     private final boolean mUseFullEditDistance;
 
+    private final SparseArray<DicTraverseSession> mDicTraverseSessions =
+            CollectionUtils.newSparseArray();
+
+    // TODO: There should be a way to remove used DicTraverseSession objects from
+    // {@code mDicTraverseSessions}.
+    private DicTraverseSession getTraverseSession(int traverseSessionId) {
+        synchronized(mDicTraverseSessions) {
+            DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId);
+            if (traverseSession == null) {
+                traverseSession = mDicTraverseSessions.get(traverseSessionId);
+                if (traverseSession == null) {
+                    traverseSession = new DicTraverseSession(mLocale, mNativeDict);
+                    mDicTraverseSessions.put(traverseSessionId, traverseSession);
+                }
+            }
+            return traverseSession;
+        }
+    }
+
     /**
      * Constructor for the binary dictionary. This is supposed to be called from the
      * dictionary factory.
@@ -74,6 +96,7 @@
             final String filename, final long offset, final long length,
             final boolean useFullEditDistance, final Locale locale, final String dictType) {
         super(dictType);
+        mLocale = locale;
         mUseFullEditDistance = useFullEditDistance;
         loadDictionary(filename, offset, length);
     }
@@ -86,18 +109,17 @@
             int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords,
             int maxPredictions);
     private native void closeNative(long dict);
-    private native int getFrequencyNative(long dict, int[] word, int wordLength);
+    private native int getFrequencyNative(long dict, int[] word);
     private native boolean isValidBigramNative(long dict, int[] word1, int[] word2);
-    private native int getSuggestionsNative(long dict, long proximityInfo, int[] xCoordinates,
-            int[] yCoordinates, int[] times, int[] pointerIds, int[] inputCodes, int codesSize,
-            int commitPoint, boolean isGesture,
+    private native int getSuggestionsNative(long dict, long proximityInfo, long traverseSession,
+            int[] xCoordinates, int[] yCoordinates, int[] times, int[] pointerIds,
+            int[] inputCodePoints, int codesSize, int commitPoint, boolean isGesture,
             int[] prevWordCodePointArray, boolean useFullEditDistance, char[] outputChars,
             int[] outputScores, int[] outputIndices, int[] outputTypes);
-    private static native float calcNormalizedScoreNative(
-            char[] before, int beforeLength, char[] after, int afterLength, int score);
-    private static native int editDistanceNative(
-            char[] before, int beforeLength, char[] after, int afterLength);
+    private static native float calcNormalizedScoreNative(char[] before, char[] after, int score);
+    private static native int editDistanceNative(char[] before, char[] after);
 
+    // TODO: Move native dict into session
     private final void loadDictionary(String path, long startOffset, long length) {
         mNativeDict = openNative(path, startOffset, length, TYPED_LETTER_MULTIPLIER,
                 FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS, MAX_PREDICTIONS);
@@ -106,10 +128,15 @@
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final CharSequence prevWord, final ProximityInfo proximityInfo) {
+        return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, 0);
+    }
+
+    @Override
+    public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) {
         if (!isValidDictionary()) return null;
-        Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE);
-        Arrays.fill(mOutputChars, (char) 0);
-        Arrays.fill(mOutputScores, 0);
+
+        Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
         // TODO: toLowerCase in the native code
         final int[] prevWordCodePointArray = (null == prevWord)
                 ? null : StringUtils.toCodePointArray(prevWord.toString());
@@ -119,7 +146,7 @@
         if (composerSize <= 1 || !isGesture) {
             if (composerSize > MAX_WORD_LENGTH - 1) return null;
             for (int i = 0; i < composerSize; i++) {
-                mInputCodes[i] = composer.getCodeAt(i);
+                mInputCodePoints[i] = composer.getCodeAt(i);
             }
         }
 
@@ -127,24 +154,25 @@
         final int codesSize = isGesture ? ips.getPointerSize() : composerSize;
         // proximityInfo and/or prevWordForBigrams may not be null.
         final int tmpCount = getSuggestionsNative(mNativeDict,
-                proximityInfo.getNativeProximityInfo(), ips.getXCoordinates(),
-                ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(),
-                mInputCodes, codesSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray,
+                proximityInfo.getNativeProximityInfo(), getTraverseSession(sessionId).getSession(),
+                ips.getXCoordinates(), ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(),
+                mInputCodePoints, codesSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray,
                 mUseFullEditDistance, mOutputChars, mOutputScores, mSpaceIndices, mOutputTypes);
         final int count = Math.min(tmpCount, MAX_PREDICTIONS);
 
-        final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<SuggestedWordInfo>();
+        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
         for (int j = 0; j < count; ++j) {
             if (composerSize > 0 && mOutputScores[j] < 1) break;
             final int start = j * MAX_WORD_LENGTH;
             int len = 0;
-            while (len <  MAX_WORD_LENGTH && mOutputChars[start + len] != 0) {
+            while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) {
                 ++len;
             }
             if (len > 0) {
+                final int score = SuggestedWordInfo.KIND_WHITELIST == mOutputTypes[j]
+                        ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j];
                 suggestions.add(new SuggestedWordInfo(
-                        new String(mOutputChars, start, len),
-                        mOutputScores[j], SuggestedWordInfo.KIND_CORRECTION, mDictType));
+                        new String(mOutputChars, start, len), score, mOutputTypes[j], mDictType));
             }
         }
         return suggestions;
@@ -155,13 +183,11 @@
     }
 
     public static float calcNormalizedScore(String before, String after, int score) {
-        return calcNormalizedScoreNative(before.toCharArray(), before.length(),
-                after.toCharArray(), after.length(), score);
+        return calcNormalizedScoreNative(before.toCharArray(), after.toCharArray(), score);
     }
 
     public static int editDistance(String before, String after) {
-        return editDistanceNative(
-                before.toCharArray(), before.length(), after.toCharArray(), after.length());
+        return editDistanceNative(before.toCharArray(), after.toCharArray());
     }
 
     @Override
@@ -172,8 +198,8 @@
     @Override
     public int getFrequency(CharSequence word) {
         if (word == null) return -1;
-        int[] chars = StringUtils.toCodePointArray(word.toString());
-        return getFrequencyNative(mNativeDict, chars, chars.length);
+        int[] codePoints = StringUtils.toCodePointArray(word.toString());
+        return getFrequencyNative(mNativeDict, codePoints);
     }
 
     // TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni
@@ -186,11 +212,20 @@
     }
 
     @Override
-    public synchronized void close() {
+    public void close() {
+        synchronized (mDicTraverseSessions) {
+            final int sessionsSize = mDicTraverseSessions.size();
+            for (int index = 0; index < sessionsSize; ++index) {
+                final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index);
+                if (traverseSession != null) {
+                    traverseSession.close();
+                }
+            }
+        }
         closeInternal();
     }
 
-    private void closeInternal() {
+    private synchronized void closeInternal() {
         if (mNativeDict != 0) {
             closeNative(mNativeDict);
             mNativeDict = 0;
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 236c198..799aea8 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -99,7 +99,7 @@
         }
 
         try {
-            final List<WordListInfo> list = new ArrayList<WordListInfo>();
+            final List<WordListInfo> list = CollectionUtils.newArrayList();
             do {
                 final String wordListId = c.getString(0);
                 final String wordListLocale = c.getString(1);
@@ -267,7 +267,7 @@
         final ContentResolver resolver = context.getContentResolver();
         final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
                 hasDefaultWordList);
-        final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
+        final List<AssetFileAddress> fileAddressList = CollectionUtils.newArrayList();
         for (WordListInfo id : idList) {
             final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context);
             if (null != afd) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 063243e..e1cb195 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin;
 
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -23,6 +25,10 @@
 import android.util.Log;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Locale;
@@ -51,6 +57,9 @@
     private static final String MAIN_DICTIONARY_CATEGORY = "main";
     public static final String ID_CATEGORY_SEPARATOR = ":";
 
+    // The key considered to read the version attribute in a dictionary file.
+    private static String VERSION_KEY = "version";
+
     // Prevents this from being instantiated
     private BinaryDictionaryGetter() {}
 
@@ -254,8 +263,7 @@
             final Context context) {
         final File[] directoryList = getCachedDirectoryList(context);
         if (null == directoryList) return EMPTY_FILE_ARRAY;
-        final HashMap<String, FileAndMatchLevel> cacheFiles =
-                new HashMap<String, FileAndMatchLevel>();
+        final HashMap<String, FileAndMatchLevel> cacheFiles = CollectionUtils.newHashMap();
         for (File directory : directoryList) {
             if (!directory.isDirectory()) continue;
             final String dirLocale = getWordListIdFromFileName(directory.getName());
@@ -336,6 +344,54 @@
         return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
     }
 
+    // ## HACK ## we prevent usage of a dictionary before version 18 for English only. The reason
+    // for this is, since those do not include whitelist entries, the new code with an old version
+    // of the dictionary would lose whitelist functionality.
+    private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) {
+        // Only for English - other languages didn't have a whitelist, hence this
+        // ad-hock ## HACK ##
+        if (!Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) return true;
+
+        FileInputStream inStream = null;
+        try {
+            // Read the version of the file
+            inStream = new FileInputStream(f);
+            final ByteBuffer buffer = inStream.getChannel().map(
+                    FileChannel.MapMode.READ_ONLY, 0, f.length());
+            final int magic = buffer.getInt();
+            if (magic != BinaryDictInputOutput.VERSION_2_MAGIC_NUMBER) {
+                return false;
+            }
+            final int formatVersion = buffer.getInt();
+            final int headerSize = buffer.getInt();
+            final HashMap<String, String> options = CollectionUtils.newHashMap();
+            BinaryDictInputOutput.populateOptions(buffer, headerSize, options);
+
+            final String version = options.get(VERSION_KEY);
+            if (null == version) {
+                // No version in the options : the format is unexpected
+                return false;
+            }
+            // Version 18 is the first one to include the whitelist
+            // Obviously this is a big ## HACK ##
+            return Integer.parseInt(version) >= 18;
+        } catch (java.io.FileNotFoundException e) {
+            return false;
+        } catch (java.io.IOException e) {
+            return false;
+        } catch (NumberFormatException e) {
+            return false;
+        } finally {
+            if (inStream != null) {
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    // do nothing
+                }
+            }
+        }
+    }
+
     /**
      * Returns a list of file addresses for a given locale, trying relevant methods in order.
      *
@@ -362,18 +418,19 @@
         final DictPackSettings dictPackSettings = new DictPackSettings(context);
 
         boolean foundMainDict = false;
-        final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
+        final ArrayList<AssetFileAddress> fileList = CollectionUtils.newArrayList();
         // cachedWordLists may not be null, see doc for getCachedDictionaryList
         for (final File f : cachedWordLists) {
             final String wordListId = getWordListIdFromFileName(f.getName());
-            if (isMainWordListId(wordListId)) {
+            final boolean canUse = f.canRead() && hackCanUseDictionaryFile(locale, f);
+            if (canUse && isMainWordListId(wordListId)) {
                 foundMainDict = true;
             }
             if (!dictPackSettings.isWordListActive(wordListId)) continue;
-            if (f.canRead()) {
+            if (canUse) {
                 fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
             } else {
-                Log.e(TAG, "Found a cached dictionary file but cannot read it");
+                Log.e(TAG, "Found a cached dictionary file but cannot read or use it");
             }
         }
 
diff --git a/java/src/com/android/inputmethod/latin/CollectionUtils.java b/java/src/com/android/inputmethod/latin/CollectionUtils.java
new file mode 100644
index 0000000..baa2ee1
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/CollectionUtils.java
@@ -0,0 +1,95 @@
+/*
+ * 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.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class CollectionUtils {
+    private CollectionUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static <K,V> HashMap<K,V> newHashMap() {
+        return new HashMap<K,V>();
+    }
+
+    public static <K,V> TreeMap<K,V> newTreeMap() {
+        return new TreeMap<K,V>();
+    }
+
+    public static <K, V> Map<K,V> newSynchronizedTreeMap() {
+        final TreeMap<K,V> treeMap = newTreeMap();
+        return Collections.synchronizedMap(treeMap);
+    }
+
+    public static <K,V> ConcurrentHashMap<K,V> newConcurrentHashMap() {
+        return new ConcurrentHashMap<K,V>();
+    }
+
+    public static <E> HashSet<E> newHashSet() {
+        return new HashSet<E>();
+    }
+
+    public static <E> TreeSet<E> newTreeSet() {
+        return new TreeSet<E>();
+    }
+
+    public static <E> ArrayList<E> newArrayList() {
+        return new ArrayList<E>();
+    }
+
+    public static <E> ArrayList<E> newArrayList(final int initialCapacity) {
+        return new ArrayList<E>(initialCapacity);
+    }
+
+    public static <E> ArrayList<E> newArrayList(final Collection<E> collection) {
+        return new ArrayList<E>(collection);
+    }
+
+    public static <E> LinkedList<E> newLinkedList() {
+        return new LinkedList<E>();
+    }
+
+    public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList() {
+        return new CopyOnWriteArrayList<E>();
+    }
+
+    public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(
+            final Collection<E> collection) {
+        return new CopyOnWriteArrayList<E>(collection);
+    }
+
+    public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(final E[] array) {
+        return new CopyOnWriteArrayList<E>(array);
+    }
+
+    public static <E> SparseArray<E> newSparseArray() {
+        return new SparseArray<E>();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 1242967..d71c0f9 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -128,6 +128,13 @@
         }
     }
 
+    public static final int NOT_A_CODE = -1;
+
+    // See {@link KeyboardActionListener.Adapter#isInvalidCoordinate(int)}.
+    public static final int NOT_A_COORDINATE = -1;
+    public static final int SUGGESTION_STRIP_COORDINATE = -2;
+    public static final int SPELL_CHECKER_COORDINATE = -3;
+
     private Constants() {
         // This utility class is not publicly instantiable.
     }
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
new file mode 100644
index 0000000..359da72
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -0,0 +1,75 @@
+/*
+ * 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 java.util.Locale;
+
+public class DicTraverseSession {
+    static {
+        JniUtils.loadNativeLibrary();
+    }
+
+    private native long setDicTraverseSessionNative(String locale);
+    private native void initDicTraverseSessionNative(long nativeDicTraverseSession,
+            long dictionary, int[] previousWord, int previousWordLength);
+    private native void releaseDicTraverseSessionNative(long nativeDicTraverseSession);
+
+    private long mNativeDicTraverseSession;
+
+    public DicTraverseSession(Locale locale, long dictionary) {
+        mNativeDicTraverseSession = createNativeDicTraverseSession(
+                locale != null ? locale.toString() : "");
+        initSession(dictionary);
+    }
+
+    public long getSession() {
+        return mNativeDicTraverseSession;
+    }
+
+    public void initSession(long dictionary) {
+        initSession(dictionary, null, 0);
+    }
+
+    public void initSession(long dictionary, int[] previousWord, int previousWordLength) {
+        initDicTraverseSessionNative(
+                mNativeDicTraverseSession, dictionary, previousWord, previousWordLength);
+    }
+
+    private final long createNativeDicTraverseSession(String locale) {
+        return setDicTraverseSessionNative(locale);
+    }
+
+    private void closeInternal() {
+        if (mNativeDicTraverseSession != 0) {
+            releaseDicTraverseSessionNative(mNativeDicTraverseSession);
+            mNativeDicTraverseSession = 0;
+        }
+    }
+
+    public void close() {
+        closeInternal();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            closeInternal();
+        } finally {
+            super.finalize();
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 60fe17b..88d0c09 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -42,7 +42,6 @@
     public static final String TYPE_USER = "user";
     // User history dictionary internal to LatinIME.
     public static final String TYPE_USER_HISTORY = "history";
-    public static final String TYPE_WHITELIST ="whitelist";
     protected final String mDictType;
 
     public Dictionary(final String dictType) {
@@ -62,6 +61,13 @@
     abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final CharSequence prevWord, final ProximityInfo proximityInfo);
 
+    // The default implementation of this method ignores sessionId.
+    // Subclasses that want to use sessionId need to override this method.
+    public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) {
+        return getSuggestions(composer, prevWord, proximityInfo);
+    }
+
     /**
      * Checks if the given word occurs in the dictionary
      * @param word the word to search for. The search should be case-insensitive.
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index ee80f25..4acab6b 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -35,22 +35,22 @@
 
     public DictionaryCollection(final String dictType) {
         super(dictType);
-        mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+        mDictionaries = CollectionUtils.newCopyOnWriteArrayList();
     }
 
     public DictionaryCollection(final String dictType, Dictionary... dictionaries) {
         super(dictType);
         if (null == dictionaries) {
-            mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+            mDictionaries = CollectionUtils.newCopyOnWriteArrayList();
         } else {
-            mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+            mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries);
             mDictionaries.removeAll(Collections.singleton(null));
         }
     }
 
     public DictionaryCollection(final String dictType, Collection<Dictionary> dictionaries) {
         super(dictType);
-        mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+        mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries);
         mDictionaries.removeAll(Collections.singleton(null));
     }
 
@@ -63,7 +63,7 @@
         // dictionary and add the rest to it if not null, hence the get(0)
         ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer,
                 prevWord, proximityInfo);
-        if (null == suggestions) suggestions = new ArrayList<SuggestedWordInfo>();
+        if (null == suggestions) suggestions = CollectionUtils.newArrayList();
         final int length = dictionaries.size();
         for (int i = 1; i < length; ++ i) {
             final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 06a5f4b..cdd01d0 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -53,7 +53,7 @@
                     createBinaryDictionary(context, locale));
         }
 
-        final LinkedList<Dictionary> dictList = new LinkedList<Dictionary>();
+        final LinkedList<Dictionary> dictList = CollectionUtils.newLinkedList();
         final ArrayList<AssetFileAddress> assetFileList =
                 BinaryDictionaryGetter.getDictionaryFiles(locale, context);
         if (null != assetFileList) {
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 016530a..cdf5247 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -62,7 +62,7 @@
      * that filename.
      */
     private static final HashMap<String, DictionaryController> sSharedDictionaryControllers =
-            new HashMap<String, DictionaryController>();
+            CollectionUtils.newHashMap();
 
     /** The application context. */
     protected final Context mContext;
@@ -159,9 +159,9 @@
      * the native side.
      */
     public void clearFusionDictionary() {
+        final HashMap<String, String> attributes = CollectionUtils.newHashMap();
         mFusionDictionary = new FusionDictionary(new Node(),
-                new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false, 
-                        false));
+                new FusionDictionary.DictionaryOptions(attributes, false, false));
     }
 
     /**
@@ -175,7 +175,7 @@
             mFusionDictionary.add(word, frequency, null);
         } else {
             // TODO: Do this in the subclass, with this class taking an arraylist.
-            final ArrayList<WeightedString> shortcutTargets = new ArrayList<WeightedString>();
+            final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
             shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
             mFusionDictionary.add(word, frequency, shortcutTargets);
         }
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index d101aaf..8a38d1e 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.text.TextUtils;
 
-import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -231,7 +230,7 @@
             childNode.mTerminal = true;
             if (isShortcutOnly) {
                 if (null == childNode.mShortcutTargets) {
-                    childNode.mShortcutTargets = new ArrayList<char[]>();
+                    childNode.mShortcutTargets = CollectionUtils.newArrayList();
                 }
                 childNode.mShortcutTargets.add(shortcutTarget.toCharArray());
             } else {
@@ -251,7 +250,7 @@
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final CharSequence prevWord, final ProximityInfo proximityInfo) {
         if (reloadDictionaryIfRequired()) return null;
-        if (composer.size() <= 1) {
+        if (composer.size() > 1) {
             if (composer.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
                 return null;
             }
@@ -260,7 +259,7 @@
             return suggestions;
         } else {
             if (TextUtils.isEmpty(prevWord)) return null;
-            final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<SuggestedWordInfo>();
+            final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
             runBigramReverseLookUp(prevWord, suggestions);
             return suggestions;
         }
@@ -279,7 +278,7 @@
 
     protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
             final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
-        final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<SuggestedWordInfo>();
+        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
         mInputLength = codes.size();
         if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
         final InputPointers ips = codes.getInputPointers();
@@ -292,9 +291,9 @@
                 mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE];
             }
             final int x = xCoordinates != null && i < xCoordinates.length ?
-                    xCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+                    xCoordinates[i] : Constants.NOT_A_COORDINATE;
             final int y = xCoordinates != null && i < yCoordinates.length ?
-                    yCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+                    yCoordinates[i] : Constants.NOT_A_COORDINATE;
             proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]);
         }
         mMaxDepth = mInputLength * 3;
@@ -487,7 +486,7 @@
                 for (int j = 0; j < alternativesSize; j++) {
                     final int addedAttenuation = (j > 0 ? 1 : 2);
                     final int currentChar = currentChars[j];
-                    if (currentChar == KeyDetector.NOT_A_CODE) {
+                    if (currentChar == Constants.NOT_A_CODE) {
                         break;
                     }
                     if (currentChar == lowerC || currentChar == c) {
@@ -551,7 +550,7 @@
         Node secondWord = searchWord(mRoots, word2, 0, null);
         LinkedList<NextWord> bigrams = firstWord.mNGrams;
         if (bigrams == null || bigrams.size() == 0) {
-            firstWord.mNGrams = new LinkedList<NextWord>();
+            firstWord.mNGrams = CollectionUtils.newLinkedList();
             bigrams = firstWord.mNGrams;
         } else {
             for (NextWord nw : bigrams) {
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index e561f59..7bcda9b 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -29,10 +29,12 @@
     final public boolean mInputTypeNoAutoCorrect;
     final public boolean mIsSettingsSuggestionStripOn;
     final public boolean mApplicationSpecifiedCompletionOn;
+    final private int mInputType;
 
     public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) {
         final int inputType = null != editorInfo ? editorInfo.inputType : 0;
         final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
+        mInputType = inputType;
         if (inputClass != InputType.TYPE_CLASS_TEXT) {
             // If we are not looking at a TYPE_CLASS_TEXT field, the following strange
             // cases may arise, so we do a couple sanity checks for them. If it's a
@@ -93,6 +95,10 @@
         }
     }
 
+    public boolean isSameInputType(final EditorInfo editorInfo) {
+        return editorInfo.inputType == mInputType;
+    }
+
     @SuppressWarnings("unused")
     private void dumpFlags(final int inputType) {
         Log.i(TAG, "Input class:");
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
index cbc916a..ff2feb5 100644
--- a/java/src/com/android/inputmethod/latin/InputPointers.java
+++ b/java/src/com/android/inputmethod/latin/InputPointers.java
@@ -93,7 +93,7 @@
         }
         mXCoordinates.append(xCoordinates, startPos, length);
         mYCoordinates.append(yCoordinates, startPos, length);
-        mPointerIds.fill(pointerId, startPos, length);
+        mPointerIds.fill(pointerId, mPointerIds.getLength(), length);
         mTimes.append(times, startPos, length);
     }
 
@@ -124,4 +124,10 @@
     public int[] getTimes() {
         return mTimes.getPrimitiveArray();
     }
+
+    @Override
+    public String toString() {
+        return "size=" + getPointerSize() + " id=" + mPointerIds + " time=" + mTimes
+                + " x=" + mXCoordinates + " y=" + mYCoordinates;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 4550860..83a3068 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -361,7 +361,7 @@
         mPrefs = prefs;
         LatinImeLogger.init(this, prefs);
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.getInstance().init(this, prefs, mKeyboardSwitcher);
+            ResearchLogger.getInstance().init(this, prefs);
         }
         InputMethodManagerCompatWrapper.init(this);
         SubtypeSwitcher.init(this);
@@ -381,18 +381,7 @@
 
         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();
-                tryGC = false;
-            } catch (OutOfMemoryError e) {
-                tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
-            }
-        }
+        initSuggest();
 
         mDisplayOrientation = res.getConfiguration().orientation;
 
@@ -416,7 +405,8 @@
     }
 
     // Has to be package-visible for unit tests
-    /* package */ void loadSettings() {
+    /* package for test */
+    void loadSettings() {
         // 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);
@@ -433,10 +423,14 @@
         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
     }
 
+    // Note that this method is called from a non-UI thread.
     @Override
     public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable) {
         mIsMainDictionaryAvailable = isMainDictionaryAvailable;
-        updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability();
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView != null) {
+            mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
+        }
     }
 
     private void initSuggest() {
@@ -517,7 +511,7 @@
 
     /* package private */ void resetSuggestMainDict() {
         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
-        mSuggest.resetMainDict(this, subtypeLocale);
+        mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
     }
 
@@ -536,7 +530,10 @@
 
     @Override
     public void onConfigurationChanged(Configuration conf) {
-        mSubtypeSwitcher.onConfigurationChanged(conf);
+        // System locale has been changed. Needs to reload keyboard.
+        if (mSubtypeSwitcher.onConfigurationChanged(conf, this)) {
+            loadKeyboard();
+        }
         // If orientation changed while predicting, commit the change
         if (mDisplayOrientation != conf.orientation) {
             mDisplayOrientation = conf.orientation;
@@ -603,6 +600,7 @@
         // 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.
         mSubtypeSwitcher.updateSubtype(subtype);
+        loadKeyboard();
     }
 
     private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) {
@@ -663,11 +661,23 @@
         // Forward this event to the accessibility utilities, if enabled.
         final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
         if (accessUtils.isTouchExplorationEnabled()) {
-            accessUtils.onStartInputViewInternal(editorInfo, restarting);
+            accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
         }
 
-        if (!restarting) {
-            mSubtypeSwitcher.updateParametersOnStartInputView();
+        final boolean selectionChanged = mLastSelectionStart != editorInfo.initialSelStart
+                || mLastSelectionEnd != editorInfo.initialSelEnd;
+        final boolean inputTypeChanged = !mCurrentSettings.isSameInputType(editorInfo);
+        final boolean isDifferentTextField = !restarting || inputTypeChanged;
+        if (isDifferentTextField) {
+            final boolean currentSubtypeEnabled = mSubtypeSwitcher
+                    .updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled();
+            if (!currentSubtypeEnabled) {
+                // Current subtype is disabled. Needs to update subtype and keyboard.
+                final InputMethodSubtype newSubtype = ImfUtils.getCurrentInputMethodSubtype(
+                        this, mSubtypeSwitcher.getNoLanguageSubtype());
+                mSubtypeSwitcher.updateSubtype(newSubtype);
+                loadKeyboard();
+            }
         }
 
         // The EditorInfo might have a flag that affects fullscreen mode.
@@ -675,9 +685,7 @@
         updateFullscreenMode();
         mApplicationSpecifiedCompletions = null;
 
-        final boolean selectionChanged = mLastSelectionStart != editorInfo.initialSelStart
-                || mLastSelectionEnd != editorInfo.initialSelEnd;
-        if (!restarting || selectionChanged) {
+        if (isDifferentTextField || selectionChanged) {
             // If the selection changed, we reset the input state. Essentially, we come here with
             // restarting == true when the app called setText() or similar. We should reset the
             // state if the app set the text to something else, but keep it if it set a suggestion
@@ -692,7 +700,7 @@
             }
         }
 
-        if (!restarting) {
+        if (isDifferentTextField) {
             mainKeyboardView.closing();
             loadSettings();
 
@@ -701,7 +709,6 @@
             }
 
             switcher.loadKeyboard(editorInfo, mCurrentSettings);
-            updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability();
         }
         setSuggestionStripShownInternal(
                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
@@ -719,8 +726,12 @@
         mHandler.cancelUpdateSuggestionStrip();
         mHandler.cancelDoubleSpacesTimer();
 
+        mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
         mainKeyboardView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
                 mCurrentSettings.mKeyPreviewPopupDismissDelay);
+        mainKeyboardView.setGestureHandlingEnabledByUser(mCurrentSettings.mGestureInputEnabled);
+        mainKeyboardView.setGesturePreviewMode(mCurrentSettings.mGesturePreviewTrailEnabled,
+                mCurrentSettings.mGestureFloatingPreviewTextEnabled);
 
         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
     }
@@ -898,13 +909,13 @@
                 }
             }
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
-        }
         if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
         mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
         if (applicationSpecifiedCompletions == null) {
             clearSuggestionStrip();
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.latinIME_onDisplayCompletions(null);
+            }
             return;
         }
 
@@ -926,6 +937,9 @@
         // this case? This says to keep whatever the user typed.
         mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
         setSuggestionStripShown(true);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
+        }
     }
 
     private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
@@ -1046,9 +1060,6 @@
         final CharSequence typedWord = mWordComposer.getTypedWord();
         if (typedWord.length() > 0) {
             mConnection.commitText(typedWord, 1);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_commitText(typedWord);
-            }
             final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
             mLastComposedWord = mWordComposer.commitWord(
                     LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
@@ -1084,18 +1095,27 @@
         return mConnection.getCursorCapsMode(inputType);
     }
 
+    // Factor in auto-caps and manual caps and compute the current caps mode.
+    private int getActualCapsMode() {
+        final int manual = mKeyboardSwitcher.getManualCapsMode();
+        if (manual != WordComposer.CAPS_MODE_OFF) return manual;
+        final int auto = getCurrentAutoCapsState();
+        if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
+            return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
+        }
+        if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
+        return WordComposer.CAPS_MODE_OFF;
+    }
+
     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) {
             mConnection.deleteSurroundingText(2, 0);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_deleteSurroundingText(2);
-            }
             mConnection.commitText(lastTwo.charAt(1) + " ", 1);
             if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
+                ResearchLogger.latinIME_swapSwapperAndSpace();
             }
             mKeyboardSwitcher.updateShiftState();
         }
@@ -1112,9 +1132,6 @@
             mHandler.cancelDoubleSpacesTimer();
             mConnection.deleteSurroundingText(2, 0);
             mConnection.commitText(". ", 1);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_doubleSpaceAutoPeriod();
-            }
             mKeyboardSwitcher.updateShiftState();
             return true;
         }
@@ -1178,9 +1195,6 @@
 
     private void performEditorAction(int actionId) {
         mConnection.performEditorAction(actionId);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_performEditorAction(actionId);
-        }
     }
 
     private void handleLanguageSwitchKey() {
@@ -1217,6 +1231,9 @@
         // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
         if (code >= '0' && code <= '9') {
             super.sendKeyChar((char)code);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.latinIME_sendKeyCodePoint(code);
+            }
             return;
         }
 
@@ -1233,9 +1250,6 @@
             final String text = new String(new int[] { code }, 0, 1);
             mConnection.commitText(text, text.length());
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_sendKeyCodePoint(code);
-        }
     }
 
     // Implementation of {@link KeyboardActionListener}.
@@ -1247,11 +1261,6 @@
         }
         mLastKeyTime = when;
         mConnection.beginBatchEdit();
-
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
-        }
-
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
         // The space state depends only on the last character pressed and its own previous
         // state. Here, we revert the space state to neutral if the key is actually modifying
@@ -1284,7 +1293,7 @@
             onSettingsKeyPressed();
             break;
         case Keyboard.CODE_SHORTCUT:
-            mSubtypeSwitcher.switchToShortcutIME();
+            mSubtypeSwitcher.switchToShortcutIME(this);
             break;
         case Keyboard.CODE_ACTION_ENTER:
             performEditorAction(getActionId(switcher.getKeyboard()));
@@ -1317,8 +1326,8 @@
                     keyX = x;
                     keyY = y;
                 } else {
-                    keyX = NOT_A_TOUCH_COORDINATE;
-                    keyY = NOT_A_TOUCH_COORDINATE;
+                    keyX = Constants.NOT_A_COORDINATE;
+                    keyY = Constants.NOT_A_COORDINATE;
                 }
                 handleCharacter(primaryCode, keyX, keyY, spaceState);
             }
@@ -1333,22 +1342,24 @@
             mLastComposedWord.deactivate();
         mEnteredText = null;
         mConnection.endBatchEdit();
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
+        }
     }
 
     // Called from PointerTracker through the KeyboardActionListener interface
     @Override
     public void onTextInput(CharSequence rawText) {
         mConnection.beginBatchEdit();
-        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+        if (mWordComposer.isComposingWord()) {
+            commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
+        }
         mHandler.postUpdateSuggestionStrip();
         final CharSequence text = specificTldProcessingOnTextInput(rawText);
         if (SPACE_STATE_PHANTOM == mSpaceState) {
             sendKeyCodePoint(Keyboard.CODE_SPACE);
         }
         mConnection.commitText(text, 1);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_commitText(text);
-        }
         mConnection.endBatchEdit();
         mKeyboardSwitcher.updateShiftState();
         mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
@@ -1361,15 +1372,14 @@
     public void onStartBatchInput() {
         mConnection.beginBatchEdit();
         if (mWordComposer.isComposingWord()) {
-            commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+            commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
             mExpectingUpdateSelection = true;
             // TODO: Can we remove this?
             mSpaceState = SPACE_STATE_PHANTOM;
         }
         mConnection.endBatchEdit();
         // TODO: Should handle TextUtils.CAP_MODE_CHARACTER.
-        mWordComposer.setAutoCapitalized(
-                getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
+        mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
     }
 
     @Override
@@ -1447,9 +1457,6 @@
             // like the smiley key or the .com key.
             final int length = mEnteredText.length();
             mConnection.deleteSurroundingText(length, 0);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_deleteSurroundingText(length);
-            }
             // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
             // In addition we know that spaceState is false, and that we should not be
             // reverting any autocorrect at this point. So we can safely return.
@@ -1469,9 +1476,6 @@
                 mHandler.postUpdateSuggestionStrip();
             } else {
                 mConnection.deleteSurroundingText(1, 0);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_deleteSurroundingText(1);
-                }
             }
         } else {
             if (mLastComposedWord.canRevertCommit()) {
@@ -1500,9 +1504,6 @@
                 final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
                 mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
                 mConnection.deleteSurroundingText(lengthToDelete, 0);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
-                }
             } else {
                 // There is no selection, just delete one character.
                 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
@@ -1521,14 +1522,8 @@
                 } else {
                     mConnection.deleteSurroundingText(1, 0);
                 }
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_deleteSurroundingText(1);
-                }
                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
                     mConnection.deleteSurroundingText(1, 0);
-                    if (ProductionFlag.IS_EXPERIMENTAL) {
-                        ResearchLogger.latinIME_deleteSurroundingText(1);
-                    }
                 }
             }
             if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
@@ -1604,13 +1599,12 @@
             mWordComposer.add(primaryCode, keyX, keyY);
             // If it's the first letter, make note of auto-caps state
             if (mWordComposer.size() == 1) {
-                mWordComposer.setAutoCapitalized(
-                        getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
+                mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
             }
             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
         } else {
             final boolean swapWeakSpace = maybeStripSpace(primaryCode,
-                    spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+                    spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
 
             sendKeyCodePoint(primaryCode);
 
@@ -1640,7 +1634,7 @@
         }
 
         final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
-                KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+                Constants.SUGGESTION_STRIP_COORDINATE == x);
 
         if (SPACE_STATE_PHANTOM == spaceState &&
                 mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
@@ -1687,6 +1681,7 @@
 
         Utils.Stats.onSeparator((char)primaryCode, x, y);
 
+        mHandler.postUpdateShiftState();
         return didAutoCorrect;
     }
 
@@ -1707,7 +1702,8 @@
 
     // TODO: make this private
     // Outside LatinIME, only used by the test suite.
-    /* package for tests */ boolean isShowingPunctuationList() {
+    /* package for tests */
+    boolean isShowingPunctuationList() {
         if (mSuggestionStripView == null) return false;
         return mCurrentSettings.mSuggestPuncList == mSuggestionStripView.getSuggestions();
     }
@@ -1853,10 +1849,6 @@
                         + "is empty? Impossible! I must commit suicide.");
             }
             Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
-                        autoCorrection.toString());
-            }
             mExpectingUpdateSelection = true;
             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
                     separatorCodePoint);
@@ -1873,8 +1865,7 @@
     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
     // interface
     @Override
-    public void pickSuggestionManually(final int index, final CharSequence suggestion,
-            final int x, final int y) {
+    public void pickSuggestionManually(final int index, final CharSequence suggestion) {
         final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions();
         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
         if (suggestion.length() == 1 && isShowingPunctuationList()) {
@@ -1882,13 +1873,12 @@
             // So, LatinImeLogger logs "" as a user's input.
             LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
             // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
-            }
             final int primaryCode = suggestion.charAt(0);
             onCodeInput(primaryCode,
-                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
-                    KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
+                    Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.latinIME_punctuationSuggestion(index, suggestion);
+            }
             return;
         }
 
@@ -1915,10 +1905,6 @@
             final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
             mConnection.commitCompletion(completionInfo);
             mConnection.endBatchEdit();
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
-                        completionInfo.getText(), x, y);
-            }
             return;
         }
 
@@ -1927,12 +1913,12 @@
         final String replacedWord = mWordComposer.getTypedWord().toString();
         LatinImeLogger.logOnManualSuggestion(replacedWord,
                 suggestion.toString(), index, suggestedWords);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
-        }
         mExpectingUpdateSelection = true;
         commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
                 LastComposedWord.NOT_A_SEPARATOR);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion);
+        }
         mConnection.endBatchEdit();
         // Don't allow cancellation of manual pick
         mLastComposedWord.deactivate();
@@ -1948,8 +1934,8 @@
                 // If the suggestion is not in the dictionary, the hint should be shown.
                 && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true);
 
-        Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
-                WordComposer.NOT_A_COORDINATE);
+        Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE,
+                Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
             mSuggestionStripView.showAddToDictionaryHint(
                     suggestion, mCurrentSettings.mHintToSaveText);
@@ -1967,9 +1953,6 @@
         final SuggestedWords suggestedWords = mSuggestionStripView.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);
         // TODO: figure out here if this is an auto-correct or if the best word is actually
@@ -1992,6 +1975,7 @@
 
     private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
         if (TextUtils.isEmpty(suggestion)) return null;
+        if (mSuggest == null) 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
@@ -2003,7 +1987,7 @@
             final CharSequence prevWord
                     = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2);
             final String secondWord;
-            if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
+            if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
                 secondWord = suggestion.toString().toLowerCase(
                         mSubtypeSwitcher.getCurrentSubtypeLocale());
             } else {
@@ -2036,9 +2020,6 @@
         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
         final int length = word.length();
         mConnection.deleteSurroundingText(length, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(length);
-        }
         mConnection.setComposingText(word, 1);
         mHandler.postUpdateSuggestionStrip();
     }
@@ -2066,9 +2047,6 @@
             }
         }
         mConnection.deleteSurroundingText(deleteLength, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
-        }
         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
             mUserHistoryDictionary.cancelAddingUserHistory(
                     previousWord.toString(), committedWord.toString());
@@ -2076,8 +2054,8 @@
         mConnection.commitText(originallyTypedWord, 1);
         // Re-insert the separator
         sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
-        Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
-                WordComposer.NOT_A_COORDINATE);
+        Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode,
+                Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_revertCommit(originallyTypedWord);
         }
@@ -2093,9 +2071,10 @@
         return mCurrentSettings.isWordSeparator(code);
     }
 
-    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
-    // according to new language or mode. Called from SubtypeSwitcher.
-    public void onRefreshKeyboard() {
+    // TODO: Make this private
+    // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
+    /* package for test */
+    void loadKeyboard() {
         // When the device locale is changed in SetupWizard etc., this method may get called via
         // onConfigurationChanged before SoftInputWindow is shown.
         initSuggest();
@@ -2103,7 +2082,6 @@
         if (mKeyboardSwitcher.getMainKeyboardView() != null) {
             // Reload keyboard because the current language has been changed.
             mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
-            updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability();
         }
         // 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
@@ -2111,17 +2089,6 @@
         mHandler.postUpdateSuggestionStrip();
     }
 
-    private void updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability() {
-        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
-        if (mainKeyboardView != null) {
-            final boolean shouldHandleGesture = mCurrentSettings.mGestureInputEnabled
-                    && mIsMainDictionaryAvailable;
-            mainKeyboardView.setGestureHandlingMode(shouldHandleGesture,
-                    mCurrentSettings.mGesturePreviewTrailEnabled,
-                    mCurrentSettings.mGestureFloatingPreviewTextEnabled);
-        }
-    }
-
     // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
     // {@link KeyboardSwitcher}. Called from KeyboardSwitcher
     public void hapticAndAudioFeedback(final int primaryCode) {
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java
index b938dd3..3b08cab 100644
--- a/java/src/com/android/inputmethod/latin/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java
@@ -193,7 +193,7 @@
         }
     }
 
-    private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>();
+    private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap();
 
     /**
      * Creates a locale from a string specification.
diff --git a/java/src/com/android/inputmethod/latin/NativeUtils.java b/java/src/com/android/inputmethod/latin/NativeUtils.java
deleted file mode 100644
index 9cc2bc0..0000000
--- a/java/src/com/android/inputmethod/latin/NativeUtils.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/ResizableIntArray.java
index 387d45a..c660f92 100644
--- a/java/src/com/android/inputmethod/latin/ResizableIntArray.java
+++ b/java/src/com/android/inputmethod/latin/ResizableIntArray.java
@@ -131,4 +131,16 @@
             mLength = endPos;
         }
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < mLength; i++) {
+            if (i != 0) {
+                sb.append(",");
+            }
+            sb.append(mArray[i]);
+        }
+        return "[" + sb + "]";
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 8b4c173..41e59e9 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -55,7 +55,9 @@
     public void beginBatchEdit() {
         if (++mNestLevel == 1) {
             mIC = mParent.getCurrentInputConnection();
-            if (null != mIC) mIC.beginBatchEdit();
+            if (null != mIC) {
+                mIC.beginBatchEdit();
+            }
         } else {
             if (DBG) {
                 throw new RuntimeException("Nest level too deep");
@@ -66,7 +68,9 @@
     }
     public void endBatchEdit() {
         if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
-        if (--mNestLevel == 0 && null != mIC) mIC.endBatchEdit();
+        if (--mNestLevel == 0 && null != mIC) {
+            mIC.endBatchEdit();
+        }
     }
 
     private void checkBatchEdit() {
@@ -79,12 +83,22 @@
 
     public void finishComposingText() {
         checkBatchEdit();
-        if (null != mIC) mIC.finishComposingText();
+        if (null != mIC) {
+            mIC.finishComposingText();
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_finishComposingText();
+            }
+        }
     }
 
     public void commitText(final CharSequence text, final int i) {
         checkBatchEdit();
-        if (null != mIC) mIC.commitText(text, i);
+        if (null != mIC) {
+            mIC.commitText(text, i);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_commitText(text, i);
+            }
+        }
     }
 
     public int getCursorCapsMode(final int inputType) {
@@ -107,37 +121,72 @@
 
     public void deleteSurroundingText(final int i, final int j) {
         checkBatchEdit();
-        if (null != mIC) mIC.deleteSurroundingText(i, j);
+        if (null != mIC) {
+            mIC.deleteSurroundingText(i, j);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
+            }
+        }
     }
 
     public void performEditorAction(final int actionId) {
         mIC = mParent.getCurrentInputConnection();
-        if (null != mIC) mIC.performEditorAction(actionId);
+        if (null != mIC) {
+            mIC.performEditorAction(actionId);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_performEditorAction(actionId);
+            }
+        }
     }
 
     public void sendKeyEvent(final KeyEvent keyEvent) {
         checkBatchEdit();
-        if (null != mIC) mIC.sendKeyEvent(keyEvent);
+        if (null != mIC) {
+            mIC.sendKeyEvent(keyEvent);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_sendKeyEvent(keyEvent);
+            }
+        }
     }
 
     public void setComposingText(final CharSequence text, final int i) {
         checkBatchEdit();
-        if (null != mIC) mIC.setComposingText(text, i);
+        if (null != mIC) {
+            mIC.setComposingText(text, i);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_setComposingText(text, i);
+            }
+        }
     }
 
     public void setSelection(final int from, final int to) {
         checkBatchEdit();
-        if (null != mIC) mIC.setSelection(from, to);
+        if (null != mIC) {
+            mIC.setSelection(from, to);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_setSelection(from, to);
+            }
+        }
     }
 
     public void commitCorrection(final CorrectionInfo correctionInfo) {
         checkBatchEdit();
-        if (null != mIC) mIC.commitCorrection(correctionInfo);
+        if (null != mIC) {
+            mIC.commitCorrection(correctionInfo);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_commitCorrection(correctionInfo);
+            }
+        }
     }
 
     public void commitCompletion(final CompletionInfo completionInfo) {
         checkBatchEdit();
-        if (null != mIC) mIC.commitCompletion(completionInfo);
+        if (null != mIC) {
+            mIC.commitCompletion(completionInfo);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_commitCompletion(completionInfo);
+            }
+        }
     }
 
     public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
@@ -315,9 +364,6 @@
         if (lastOne != null && lastOne.length() == 1
                 && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
             deleteSurroundingText(1, 0);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_deleteSurroundingText(1);
-            }
         }
     }
 
@@ -382,13 +428,7 @@
             return false;
         }
         deleteSurroundingText(2, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(2);
-        }
         commitText("  ", 1);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
-        }
         return true;
     }
 
@@ -409,13 +449,7 @@
             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/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index 0843bdb..dcd2532 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -185,7 +185,7 @@
 
     // Helper functions to create member values.
     private static SuggestedWords createSuggestPuncList(final String[] puncs) {
-        final ArrayList<SuggestedWordInfo> puncList = new ArrayList<SuggestedWordInfo>();
+        final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList();
         if (puncs != null) {
             for (final String puncSpec : puncs) {
                 puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
@@ -417,6 +417,10 @@
         prefs.edit().putString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply();
     }
 
+    public boolean isSameInputType(final EditorInfo editorInfo) {
+        return mInputAttributes.isSameInputType(editorInfo);
+    }
+
     // For debug.
     public String getInputAttributesDebugString() {
         return mInputAttributes.toString();
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index 6e7d985..39c59b4 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -53,7 +53,7 @@
         if (TextUtils.isEmpty(csv)) return "";
         final String[] elements = csv.split(",");
         if (!containsInArray(key, elements)) return csv;
-        final ArrayList<String> result = new ArrayList<String>(elements.length - 1);
+        final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1);
         for (final String element : elements) {
             if (!key.equals(element)) result.add(element);
         }
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
index 21c9c0d..de5f515 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -45,13 +45,13 @@
     private static String[] sPredefinedKeyboardLayoutSet;
     // Keyboard layout to its display name map.
     private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap =
-            new HashMap<String, String>();
+            CollectionUtils.newHashMap();
     // Keyboard layout to subtype name resource id map.
     private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap =
-            new HashMap<String, Integer>();
+            CollectionUtils.newHashMap();
     // Exceptional locale to subtype name resource id map.
     private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap =
-            new HashMap<String, Integer>();
+            CollectionUtils.newHashMap();
     private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX =
             "string/subtype_generic_";
     private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX =
@@ -60,11 +60,11 @@
             "string/subtype_no_language_";
     // Exceptional locales to display name map.
     private static final HashMap<String, String> sExceptionalDisplayNamesMap =
-            new HashMap<String, String>();
+            CollectionUtils.newHashMap();
     // Keyboard layout set name for the subtypes that don't have a keyboardLayoutSet extra value.
     // This is for compatibility to keep the same subtype ids as pre-JellyBean.
     private static final HashMap<String,String> sLocaleAndExtraValueToKeyboardLayoutSetMap =
-            new HashMap<String,String>();
+            CollectionUtils.newHashMap();
 
     private SubtypeLocale() {
         // Intentional empty constructor for utility class.
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index a7a5fcb..c693edc 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.inputmethodservice.InputMethodService;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.os.AsyncTask;
@@ -42,7 +43,6 @@
     private static final String TAG = SubtypeSwitcher.class.getSimpleName();
 
     private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
-    private /* final */ LatinIME mService;
     private /* final */ InputMethodManager mImm;
     private /* final */ Resources mResources;
     private /* final */ ConnectivityManager mConnectivityManager;
@@ -68,11 +68,11 @@
             return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage;
         }
 
-        public void updateEnabledSubtypeCount(int count) {
+        public void updateEnabledSubtypeCount(final int count) {
             mEnabledSubtypeCount = count;
         }
 
-        public void updateIsSystemLanguageSameAsInputLanguage(boolean isSame) {
+        public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) {
             mIsSystemLanguageSameAsInputLanguage = isSame;
         }
     }
@@ -81,18 +81,17 @@
         return sInstance;
     }
 
-    public static void init(LatinIME service) {
-        SubtypeLocale.init(service);
-        sInstance.initialize(service);
-        sInstance.updateAllParameters();
+    public static void init(final Context context) {
+        SubtypeLocale.init(context);
+        sInstance.initialize(context);
+        sInstance.updateAllParameters(context);
     }
 
     private SubtypeSwitcher() {
         // Intentional empty constructor for singleton.
     }
 
-    private void initialize(LatinIME service) {
-        mService = service;
+    private void initialize(final Context service) {
         mResources = service.getResources();
         mImm = ImfUtils.getInputMethodManager(service);
         mConnectivityManager = (ConnectivityManager) service.getSystemService(
@@ -111,39 +110,46 @@
 
     // Update all parameters stored in SubtypeSwitcher.
     // Only configuration changed event is allowed to call this because this is heavy.
-    private void updateAllParameters() {
+    private void updateAllParameters(final Context context) {
         mCurrentSystemLocale = mResources.getConfiguration().locale;
-        updateSubtype(ImfUtils.getCurrentInputMethodSubtype(mService, mNoLanguageSubtype));
-        updateParametersOnStartInputView();
+        updateSubtype(ImfUtils.getCurrentInputMethodSubtype(context, mNoLanguageSubtype));
+        updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled();
     }
 
-    // Update parameters which are changed outside LatinIME. This parameters affect UI so they
-    // should be updated every time onStartInputview.
-    public void updateParametersOnStartInputView() {
-        updateEnabledSubtypes();
+    /**
+     * Update parameters which are changed outside LatinIME. This parameters affect UI so they
+     * should be updated every time onStartInputView.
+     *
+     * @return true if the current subtype is enabled.
+     */
+    public boolean updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled() {
+        final boolean currentSubtypeEnabled =
+                updateEnabledSubtypesAndReturnIfEnabled(mCurrentSubtype);
         updateShortcutIME();
+        return currentSubtypeEnabled;
     }
 
-    // Reload enabledSubtypes from the framework.
-    private void updateEnabledSubtypes() {
-        final InputMethodSubtype currentSubtype = mCurrentSubtype;
-        boolean foundCurrentSubtypeBecameDisabled = true;
+    /**
+     * Update enabled subtypes from the framework.
+     *
+     * @param subtype the subtype to be checked
+     * @return true if the {@code subtype} is enabled.
+     */
+    private boolean updateEnabledSubtypesAndReturnIfEnabled(final InputMethodSubtype subtype) {
         final List<InputMethodSubtype> enabledSubtypesOfThisIme =
                 mImm.getEnabledInputMethodSubtypeList(null, true);
-        for (InputMethodSubtype ims : enabledSubtypesOfThisIme) {
-            if (ims.equals(currentSubtype)) {
-                foundCurrentSubtypeBecameDisabled = false;
-            }
-        }
         mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size());
-        if (foundCurrentSubtypeBecameDisabled) {
-            if (DBG) {
-                Log.w(TAG, "Last subtype: "
-                        + currentSubtype.getLocale() + "/" + currentSubtype.getExtraValue());
-                Log.w(TAG, "Last subtype was disabled. Update to the current one.");
+
+        for (final InputMethodSubtype ims : enabledSubtypesOfThisIme) {
+            if (ims.equals(subtype)) {
+                return true;
             }
-            updateSubtype(ImfUtils.getCurrentInputMethodSubtype(mService, mNoLanguageSubtype));
         }
+        if (DBG) {
+            Log.w(TAG, "Subtype: " + subtype.getLocale() + "/" + subtype.getExtraValue()
+                    + " was disabled");
+        }
+        return false;
     }
 
     private void updateShortcutIME() {
@@ -159,8 +165,8 @@
                 mImm.getShortcutInputMethodsAndSubtypes();
         mShortcutInputMethodInfo = null;
         mShortcutSubtype = null;
-        for (InputMethodInfo imi : shortcuts.keySet()) {
-            List<InputMethodSubtype> subtypes = shortcuts.get(imi);
+        for (final InputMethodInfo imi : shortcuts.keySet()) {
+            final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
             // TODO: Returns the first found IMI for now. Should handle all shortcuts as
             // appropriate.
             mShortcutInputMethodInfo = imi;
@@ -194,24 +200,24 @@
 
         mCurrentSubtype = newSubtype;
         updateShortcutIME();
-        mService.onRefreshKeyboard();
     }
 
     ////////////////////////////
     // Shortcut IME functions //
     ////////////////////////////
 
-    public void switchToShortcutIME() {
+    public void switchToShortcutIME(final InputMethodService context) {
         if (mShortcutInputMethodInfo == null) {
             return;
         }
 
         final String imiId = mShortcutInputMethodInfo.getId();
-        switchToTargetIME(imiId, mShortcutSubtype);
+        switchToTargetIME(imiId, mShortcutSubtype, context);
     }
 
-    private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype) {
-        final IBinder token = mService.getWindow().getWindow().getAttributes().token;
+    private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
+            final InputMethodService context) {
+        final IBinder token = context.getWindow().getWindow().getAttributes().token;
         if (token == null) {
             return;
         }
@@ -253,7 +259,7 @@
         return true;
     }
 
-    public void onNetworkStateChanged(Intent intent) {
+    public void onNetworkStateChanged(final Intent intent) {
         final boolean noConnection = intent.getBooleanExtra(
                 ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
         mIsNetworkConnected = !noConnection;
@@ -265,7 +271,7 @@
     // Subtype Switching functions //
     //////////////////////////////////
 
-    public boolean needsToDisplayLanguage(Locale keyboardLocale) {
+    public boolean needsToDisplayLanguage(final Locale keyboardLocale) {
         if (keyboardLocale.toString().equals(SubtypeLocale.NO_LANGUAGE)) {
             return true;
         }
@@ -279,12 +285,14 @@
         return SubtypeLocale.getSubtypeLocale(mCurrentSubtype);
     }
 
-    public void onConfigurationChanged(Configuration conf) {
+    public boolean onConfigurationChanged(final Configuration conf, final Context context) {
         final Locale systemLocale = conf.locale;
+        final boolean systemLocaleChanged = !systemLocale.equals(mCurrentSystemLocale);
         // If system configuration was changed, update all parameters.
-        if (!systemLocale.equals(mCurrentSystemLocale)) {
-            updateAllParameters();
+        if (systemLocaleChanged) {
+            updateAllParameters(context);
         }
+        return systemLocaleChanged;
     }
 
     public InputMethodSubtype getCurrentSubtype() {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 5e2a041..51ed096 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -50,9 +50,8 @@
 
     private Dictionary mMainDictionary;
     private ContactsBinaryDictionary mContactsDict;
-    private WhitelistDictionary mWhiteListDictionary;
     private final ConcurrentHashMap<String, Dictionary> mDictionaries =
-            new ConcurrentHashMap<String, Dictionary>();
+            CollectionUtils.newConcurrentHashMap();
 
     public static final int MAX_SUGGESTIONS = 18;
 
@@ -60,13 +59,11 @@
 
     // Locale used for upper- and title-casing words
     private final Locale mLocale;
-    private final SuggestInitializationListener mListener;
 
     public Suggest(final Context context, final Locale locale,
             final SuggestInitializationListener listener) {
-        initAsynchronously(context, locale);
+        initAsynchronously(context, locale, listener);
         mLocale = locale;
-        mListener = listener;
     }
 
     /* package for test */ Suggest(final Context context, final File dictionary,
@@ -74,23 +71,13 @@
         final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
                 startOffset, length /* useFullEditDistance */, false, locale);
         mLocale = locale;
-        mListener = null;
         mMainDictionary = mainDict;
         addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict);
-        initWhitelistAndAutocorrectAndPool(context, locale);
     }
 
-    private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) {
-        mWhiteListDictionary = new WhitelistDictionary(context, locale);
-        addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_WHITELIST, mWhiteListDictionary);
-    }
-
-    private void initAsynchronously(final Context context, final Locale locale) {
-        resetMainDict(context, locale);
-
-        // TODO: read the whitelist and init the pool asynchronously too.
-        // initPool should be done asynchronously now that the pool is thread-safe.
-        initWhitelistAndAutocorrectAndPool(context, locale);
+    private void initAsynchronously(final Context context, final Locale locale,
+            final SuggestInitializationListener listener) {
+        resetMainDict(context, locale, listener);
     }
 
     private static void addOrReplaceDictionary(
@@ -104,10 +91,11 @@
         }
     }
 
-    public void resetMainDict(final Context context, final Locale locale) {
+    public void resetMainDict(final Context context, final Locale locale,
+            final SuggestInitializationListener listener) {
         mMainDictionary = null;
-        if (mListener != null) {
-            mListener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+        if (listener != null) {
+            listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
         }
         new Thread("InitializeBinaryDictionary") {
             @Override
@@ -116,8 +104,8 @@
                         DictionaryFactory.createMainDictionaryFromManager(context, locale);
                 addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, newMainDict);
                 mMainDictionary = newMainDict;
-                if (mListener != null) {
-                    mListener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+                if (listener != null) {
+                    listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
                 }
             }
         }.start();
@@ -170,9 +158,17 @@
     public SuggestedWords getSuggestedWords(
             final WordComposer wordComposer, CharSequence prevWordForBigram,
             final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) {
+        return getSuggestedWordsWithSessionId(
+                wordComposer, prevWordForBigram, proximityInfo, isCorrectionEnabled, 0);
+    }
+
+    public SuggestedWords getSuggestedWordsWithSessionId(
+            final WordComposer wordComposer, CharSequence prevWordForBigram,
+            final ProximityInfo proximityInfo, final boolean isCorrectionEnabled, int sessionId) {
         LatinImeLogger.onStartSuggestion(prevWordForBigram);
         if (wordComposer.isBatchMode()) {
-            return getSuggestedWordsForBatchInput(wordComposer, prevWordForBigram, proximityInfo);
+            return getSuggestedWordsForBatchInput(
+                    wordComposer, prevWordForBigram, proximityInfo, sessionId);
         } else {
             return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
                     isCorrectionEnabled);
@@ -209,23 +205,20 @@
                     wordComposerForLookup, prevWordForBigram, proximityInfo));
         }
 
-        // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
-        // but still autocorrected from - in the case the whitelist only capitalizes the word.
-        // The whitelist should be case-insensitive, so it's not possible to be consistent with
-        // a boolean flag. Right now this is handled with a slight hack in
-        // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
-        final boolean allowsToBeAutoCorrected = AutoCorrection.isWhitelistedOrNotAWord(
-                mDictionaries, consideredWord, wordComposer.isFirstCharCapitalized());
-
-        final CharSequence whitelistedWord =
-                mWhiteListDictionary.getWhitelistedWord(consideredWord);
-        if (whitelistedWord != null) {
-            // MAX_SCORE ensures this will be considered strong enough to be auto-corrected
-            suggestionsSet.add(new SuggestedWordInfo(whitelistedWord,
-                    SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST,
-                    Dictionary.TYPE_WHITELIST));
+        final CharSequence whitelistedWord;
+        if (suggestionsSet.isEmpty()) {
+            whitelistedWord = null;
+        } else if (SuggestedWordInfo.KIND_WHITELIST != suggestionsSet.first().mKind) {
+            whitelistedWord = null;
+        } else {
+            whitelistedWord = suggestionsSet.first().mWord;
         }
 
+        final boolean allowsToBeAutoCorrected = (null != whitelistedWord
+                && !whitelistedWord.equals(consideredWord))
+                || AutoCorrection.isNotAWord(mDictionaries, consideredWord,
+                        wordComposer.isFirstCharCapitalized());
+
         final boolean hasAutoCorrection;
         // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because
         // any attempt to do auto-correction is already shielded with a test for this flag; at the
@@ -249,7 +242,7 @@
         }
 
         final ArrayList<SuggestedWordInfo> suggestionsContainer =
-                new ArrayList<SuggestedWordInfo>(suggestionsSet);
+                CollectionUtils.newArrayList(suggestionsSet);
         final int suggestionsCount = suggestionsContainer.size();
         final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
         final boolean isAllUpperCase = wordComposer.isAllUpperCase();
@@ -296,29 +289,28 @@
     // Retrieves suggestions for the batch input.
     private SuggestedWords getSuggestedWordsForBatchInput(
             final WordComposer wordComposer, CharSequence prevWordForBigram,
-            final ProximityInfo proximityInfo) {
+            final ProximityInfo proximityInfo, int sessionId) {
         final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
                 MAX_SUGGESTIONS);
 
         // At second character typed, search the unigrams (scores being affected by bigrams)
         for (final String key : mDictionaries.keySet()) {
-            // Skip UserUnigramDictionary and WhitelistDictionary to lookup
-            if (key.equals(Dictionary.TYPE_USER_HISTORY)
-                    || key.equals(Dictionary.TYPE_WHITELIST)) {
+            // Skip User history dictionary for lookup
+            // TODO: The user history dictionary should just override getSuggestionsWithSessionId
+            // to make sure it doesn't return anything and we should remove this test
+            if (key.equals(Dictionary.TYPE_USER_HISTORY)) {
                 continue;
             }
             final Dictionary dictionary = mDictionaries.get(key);
-            suggestionsSet.addAll(dictionary.getSuggestions(
-                    wordComposer, prevWordForBigram, proximityInfo));
+            suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(
+                    wordComposer, prevWordForBigram, proximityInfo, sessionId));
         }
 
         final ArrayList<SuggestedWordInfo> suggestionsContainer =
-                new ArrayList<SuggestedWordInfo>(suggestionsSet);
+                CollectionUtils.newArrayList(suggestionsSet);
         final int suggestionsCount = suggestionsContainer.size();
-        final boolean isFirstCharCapitalized = wordComposer.isAutoCapitalized();
-        // TODO: Handle the manual temporary shifted mode.
-        // TODO: Should handle TextUtils.CAP_MODE_CHARACTER.
-        final boolean isAllUpperCase = false;
+        final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock();
+        final boolean isAllUpperCase = wordComposer.isAllUpperCase();
         if (isFirstCharCapitalized || isAllUpperCase) {
             for (int i = 0; i < suggestionsCount; ++i) {
                 final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
@@ -346,7 +338,7 @@
         typedWordInfo.setDebugString("+");
         final int suggestionsSize = suggestions.size();
         final ArrayList<SuggestedWordInfo> suggestionsList =
-                new ArrayList<SuggestedWordInfo>(suggestionsSize);
+                CollectionUtils.newArrayList(suggestionsSize);
         suggestionsList.add(typedWordInfo);
         // Note: i here is the index in mScores[], but the index in mSuggestions is one more
         // than i because we added the typed word to mSuggestions without touching mScores.
@@ -399,7 +391,7 @@
     }
 
     public void close() {
-        final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>();
+        final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
         dictionaries.addAll(mDictionaries.values());
         for (final Dictionary dictionary : dictionaries) {
             dictionary.close();
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 88fc006..68ecfa0 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -24,8 +24,10 @@
 import java.util.HashSet;
 
 public class SuggestedWords {
+    private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST =
+            CollectionUtils.newArrayList(0);
     public static final SuggestedWords EMPTY = new SuggestedWords(
-            new ArrayList<SuggestedWordInfo>(0), false, false, false, false, false);
+            EMPTY_WORD_INFO_LIST, false, false, false, false, false);
 
     public final boolean mTypedWordValid;
     // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition
@@ -83,7 +85,7 @@
 
     public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions(
             final CompletionInfo[] infos) {
-        final ArrayList<SuggestedWordInfo> result = new ArrayList<SuggestedWordInfo>();
+        final ArrayList<SuggestedWordInfo> result = CollectionUtils.newArrayList();
         for (CompletionInfo info : infos) {
             if (null != info && info.getText() != null) {
                 result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE,
@@ -97,8 +99,8 @@
     // and replace it with what the user currently typed.
     public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
             final CharSequence typedWord, final SuggestedWords previousSuggestions) {
-        final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<SuggestedWordInfo>();
-        final HashSet<String> alreadySeen = new HashSet<String>();
+        final ArrayList<SuggestedWordInfo> suggestionsList = CollectionUtils.newArrayList();
+        final HashSet<String> alreadySeen = CollectionUtils.newHashSet();
         suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
                 SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_USER_TYPED));
         alreadySeen.add(typedWord.toString());
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
index 3bb670c..6c9d1c2 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
@@ -52,14 +52,14 @@
     private static final int FREQUENCY_FOR_TYPED = 2;
 
     /** Maximum number of pairs. Pruning will start when databases goes above this number. */
-    private static int sMaxHistoryBigrams = 10000;
+    public static final int sMaxHistoryBigrams = 10000;
 
     /**
      * When it hits maximum bigram pair, it will delete until you are left with
      * only (sMaxHistoryBigrams - sDeleteHistoryBigrams) pairs.
      * Do not keep this number small to avoid deleting too often.
      */
-    private static int sDeleteHistoryBigrams = 1000;
+    public static final int sDeleteHistoryBigrams = 1000;
 
     /**
      * Database version should increase if the database structure changes
@@ -93,10 +93,10 @@
 
     private final static HashMap<String, String> sDictProjectionMap;
     private final static ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
-            sLangDictCache = new ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>();
+            sLangDictCache = CollectionUtils.newConcurrentHashMap();
 
     static {
-        sDictProjectionMap = new HashMap<String, String>();
+        sDictProjectionMap = CollectionUtils.newHashMap();
         sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID);
         sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1);
         sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2);
@@ -109,12 +109,8 @@
 
     private static DatabaseHelper sOpenHelper = null;
 
-    public void setDatabaseMax(int maxHistoryBigram) {
-        sMaxHistoryBigrams = maxHistoryBigram;
-    }
-
-    public void setDatabaseDelete(int deleteHistoryBigram) {
-        sDeleteHistoryBigrams = deleteHistoryBigram;
+    public String getLocale() {
+        return mLocale;
     }
 
     public synchronized static UserHistoryDictionary getInstance(
@@ -502,9 +498,11 @@
                                     needsToSave(fc, isValid, addLevel0Bigram)) {
                                 freq = fc;
                             } else {
+                                // Delete this entry
                                 freq = -1;
                             }
                         } else {
+                            // Delete this entry
                             freq = -1;
                         }
                     }
@@ -541,6 +539,7 @@
                                     getContentValues(word1, word2, mLocale));
                             pairId = pairIdLong.intValue();
                         }
+                        // Eliminate freq == 0 because that word is profanity.
                         if (freq > 0) {
                             if (PROFILE_SAVE_RESTORE) {
                                 ++profInsert;
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
index 610652a..bb0f542 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
@@ -29,9 +29,8 @@
 public class UserHistoryDictionaryBigramList {
     public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0;
     private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName();
-    private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = new HashMap<String, Byte>();
-    private final HashMap<String, HashMap<String, Byte>> mBigramMap =
-            new HashMap<String, HashMap<String, Byte>>();
+    private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = CollectionUtils.newHashMap();
+    private final HashMap<String, HashMap<String, Byte>> mBigramMap = CollectionUtils.newHashMap();
     private int mSize = 0;
 
     public void evictAll() {
@@ -57,7 +56,7 @@
         if (mBigramMap.containsKey(word1)) {
             map = mBigramMap.get(word1);
         } else {
-            map = new HashMap<String, Byte>();
+            map = CollectionUtils.newHashMap();
             mBigramMap.put(word1, map);
         }
         if (!map.containsKey(word2)) {
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
index 1de95d7..5a2fdf4 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
@@ -212,7 +212,7 @@
                 for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
                     final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
                     final float freq = initialFreq
-                            * NativeUtils.powf(initialFreq, elapsedHours / HALF_LIFE_HOURS);
+                            * (float)Math.pow(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 c6b5c33..fc7a421 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -65,44 +65,6 @@
         }
     }
 
-    public static class GCUtils {
-        private static final String GC_TAG = GCUtils.class.getSimpleName();
-        public static final int GC_TRY_COUNT = 2;
-        // GC_TRY_LOOP_MAX is used for the hard limit of GC wait,
-        // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT.
-        public static final int GC_TRY_LOOP_MAX = 5;
-        private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS;
-        private static GCUtils sInstance = new GCUtils();
-        private int mGCTryCount = 0;
-
-        public static GCUtils getInstance() {
-            return sInstance;
-        }
-
-        public void reset() {
-            mGCTryCount = 0;
-        }
-
-        public boolean tryGCOrWait(String metaData, Throwable t) {
-            if (mGCTryCount == 0) {
-                System.gc();
-            }
-            if (++mGCTryCount > GC_TRY_COUNT) {
-                LatinImeLogger.logOnException(metaData, t);
-                return false;
-            } else {
-                try {
-                    Thread.sleep(GC_INTERVAL);
-                    return true;
-                } catch (InterruptedException e) {
-                    Log.e(GC_TAG, "Sleep was interrupted.");
-                    LatinImeLogger.logOnException(metaData, t);
-                    return false;
-                }
-            }
-        }
-    }
-
     /* package */ static class RingCharBuffer {
         private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
         private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
@@ -477,7 +439,7 @@
 
     private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
     private static final HashMap<String, String> sDeviceOverrideValueMap =
-            new HashMap<String, String>();
+            CollectionUtils.newHashMap();
 
     public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
         final int orientation = res.getConfiguration().orientation;
@@ -495,7 +457,7 @@
         return sDeviceOverrideValueMap.get(key);
     }
 
-    private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = new HashMap<String, Long>();
+    private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap();
     private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
     public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
         if (TextUtils.isEmpty(str)) {
@@ -506,7 +468,7 @@
         if (N < 2 || N % 2 != 0) {
             return EMPTY_LT_HASH_MAP;
         }
-        final HashMap<String, Long> retval = new HashMap<String, Long>();
+        final HashMap<String, Long> retval = CollectionUtils.newHashMap();
         for (int i = 0; i < N / 2; ++i) {
             final String localeStr = ss[i * 2];
             final long time = Long.valueOf(ss[i * 2 + 1]);
diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
deleted file mode 100644
index 14476dc..0000000
--- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
+++ /dev/null
@@ -1,119 +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 android.content.res.Resources;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Locale;
-
-public class WhitelistDictionary extends ExpandableDictionary {
-
-    private static final boolean DBG = LatinImeLogger.sDBG;
-    private static final String TAG = WhitelistDictionary.class.getSimpleName();
-
-    private final HashMap<String, Pair<Integer, String>> mWhitelistWords =
-            new HashMap<String, Pair<Integer, String>>();
-
-    // TODO: Conform to the async load contact of ExpandableDictionary
-    public WhitelistDictionary(final Context context, final Locale locale) {
-        super(context, Dictionary.TYPE_WHITELIST);
-        // TODO: Move whitelist dictionary into main dictionary.
-        final RunInLocale<Void> job = new RunInLocale<Void>() {
-            @Override
-            protected Void job(Resources res) {
-                initWordlist(res.getStringArray(R.array.wordlist_whitelist));
-                return null;
-            }
-        };
-        job.runInLocale(context.getResources(), locale);
-    }
-
-    private void initWordlist(String[] wordlist) {
-        mWhitelistWords.clear();
-        final int N = wordlist.length;
-        if (N % 3 != 0) {
-            if (DBG) {
-                Log.d(TAG, "The number of the whitelist is invalid.");
-            }
-            return;
-        }
-        try {
-            for (int i = 0; i < N; i += 3) {
-                final int score = Integer.valueOf(wordlist[i]);
-                final String before = wordlist[i + 1];
-                final String after = wordlist[i + 2];
-                if (before != null && after != null) {
-                    mWhitelistWords.put(
-                            before.toLowerCase(), new Pair<Integer, String>(score, after));
-                    addWord(after, null /* shortcut */, score);
-                }
-            }
-        } catch (NumberFormatException e) {
-            if (DBG) {
-                Log.d(TAG, "The score of the word is invalid.");
-            }
-        }
-    }
-
-    public String getWhitelistedWord(String before) {
-        if (before == null) return null;
-        final String lowerCaseBefore = before.toLowerCase();
-        if(mWhitelistWords.containsKey(lowerCaseBefore)) {
-            if (DBG) {
-                Log.d(TAG, "--- found whitelistedWord: " + lowerCaseBefore);
-            }
-            return mWhitelistWords.get(lowerCaseBefore).second;
-        }
-        return null;
-    }
-
-    @Override
-    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
-            final CharSequence prevWord, final ProximityInfo proximityInfo) {
-        // Whitelist does not supply any suggestions or predictions.
-        return null;
-    }
-
-    // See LatinIME#updateSuggestions. This breaks in the (queer) case that the whitelist
-    // lists that word a should autocorrect to word b, and word c would autocorrect to
-    // an upper-cased version of a. In this case, the way this return value is used would
-    // remove the first candidate when the user typed the upper-cased version of A.
-    // Example : abc -> def  and  xyz -> Abc
-    // A user typing Abc would experience it being autocorrected to something else (not
-    // necessarily def).
-    // There is no such combination in the whitelist at the time and there probably won't
-    // ever be - it doesn't make sense. But still.
-    public boolean shouldForciblyAutoCorrectFrom(CharSequence word) {
-        if (TextUtils.isEmpty(word)) return false;
-        final String correction = getWhitelistedWord(word.toString());
-        if (TextUtils.isEmpty(correction)) return false;
-        return !correction.equals(word);
-    }
-
-    // Leave implementation of getWords and isValidWord to the superclass.
-    // The words have been added to the ExpandableDictionary with addWord() inside initWordlist.
-}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 5606a58..ecec60f 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -17,7 +17,6 @@
 package com.android.inputmethod.latin;
 
 import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
 
 import java.util.Arrays;
@@ -26,12 +25,16 @@
  * A place to store the currently composing word with information such as adjacent key codes as well
  */
 public class WordComposer {
-
-    public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
-    public static final int NOT_A_COORDINATE = -1;
-
     private static final int N = BinaryDictionary.MAX_WORD_LENGTH;
 
+    public static final int CAPS_MODE_OFF = 0;
+    // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
+    // aren't used anywhere in the code
+    public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
+    public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
+    public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
+    public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
+
     private int[] mPrimaryKeyCodes;
     private final InputPointers mInputPointers = new InputPointers(N);
     private final StringBuilder mTypedWord;
@@ -42,7 +45,7 @@
     // Cache these values for performance
     private int mCapsCount;
     private int mDigitsCount;
-    private boolean mAutoCapitalized;
+    private int mCapitalizedMode;
     private int mTrailingSingleQuotesCount;
     private int mCodePointSize;
 
@@ -68,7 +71,7 @@
         mCapsCount = source.mCapsCount;
         mDigitsCount = source.mDigitsCount;
         mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
-        mAutoCapitalized = source.mAutoCapitalized;
+        mCapitalizedMode = source.mCapitalizedMode;
         mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
         mIsResumed = source.mIsResumed;
         mIsBatchMode = source.mIsBatchMode;
@@ -166,7 +169,7 @@
             final int codePoint = Character.codePointAt(word, i);
             // We don't want to override the batch input points that are held in mInputPointers
             // (See {@link #add(int,int,int)}).
-            add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE);
+            add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         }
     }
 
@@ -181,7 +184,7 @@
             add(codePoint, x, y);
             return;
         }
-        add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE);
+        add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
     }
 
     /**
@@ -262,7 +265,14 @@
      * @return true if all user typed chars are upper case, false otherwise
      */
     public boolean isAllUpperCase() {
-        return (mCapsCount > 0) && (mCapsCount == size());
+        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+                || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED
+                || (mCapsCount > 0) && (mCapsCount == size());
+    }
+
+    public boolean wasShiftedNoLock() {
+        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
+                || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
     }
 
     /**
@@ -280,20 +290,27 @@
     }
 
     /**
-     * Saves the reason why the word is capitalized - whether it was automatic or
-     * due to the user hitting shift in the middle of a sentence.
-     * @param auto whether it was an automatic capitalization due to start of sentence
+     * Saves the caps mode at the start of composing.
+     *
+     * WordComposer needs to know about this for several reasons. The first is, we need to know
+     * after the fact what the reason was, to register the correct form into the user history
+     * dictionary: if the word was automatically capitalized, we should insert it in all-lower
+     * case but if it's a manual pressing of shift, then it should be inserted as is.
+     * Also, batch input needs to know about the current caps mode to display correctly
+     * capitalized suggestions.
+     * @param mode the mode at the time of start
      */
-    public void setAutoCapitalized(boolean auto) {
-        mAutoCapitalized = auto;
+    public void setCapitalizedModeAtStartComposingTime(final int mode) {
+        mCapitalizedMode = mode;
     }
 
     /**
      * Returns whether the word was automatically capitalized.
      * @return whether the word was automatically capitalized
      */
-    public boolean isAutoCapitalized() {
-        return mAutoCapitalized;
+    public boolean wasAutoCapitalized() {
+        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+                || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
index 2c3eee7..161b94c 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -22,10 +22,13 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 
 import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -124,7 +127,7 @@
      */
 
     private static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
-    private static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
+    public static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
     private static final int MINIMUM_SUPPORTED_VERSION = 1;
     private static final int MAXIMUM_SUPPORTED_VERSION = 2;
     private static final int NOT_A_VERSION_NUMBER = -1;
@@ -307,33 +310,32 @@
         }
 
         /**
-         * Reads a string from a RandomAccessFile. This is the converse of the above method.
+         * Reads a string from a ByteBuffer. This is the converse of the above method.
          */
-        private static String readString(final RandomAccessFile source) throws IOException {
+        private static String readString(final ByteBuffer buffer) {
             final StringBuilder s = new StringBuilder();
-            int character = readChar(source);
+            int character = readChar(buffer);
             while (character != INVALID_CHARACTER) {
                 s.appendCodePoint(character);
-                character = readChar(source);
+                character = readChar(buffer);
             }
             return s.toString();
         }
 
         /**
-         * Reads a character from the file.
+         * Reads a character from the ByteBuffer.
          *
          * This follows the character format documented earlier in this source file.
          *
-         * @param source the file, positioned over an encoded character.
+         * @param buffer the buffer, positioned over an encoded character.
          * @return the character code.
          */
-        private static int readChar(RandomAccessFile source) throws IOException {
-            int character = source.readUnsignedByte();
+        private static int readChar(final ByteBuffer buffer) {
+            int character = readUnsignedByte(buffer);
             if (!fitsOnOneByte(character)) {
-                if (GROUP_CHARACTERS_TERMINATOR == character)
-                    return INVALID_CHARACTER;
+                if (GROUP_CHARACTERS_TERMINATOR == character) return INVALID_CHARACTER;
                 character <<= 16;
-                character += source.readUnsignedShort();
+                character += readUnsignedShort(buffer);
             }
             return character;
         }
@@ -783,10 +785,10 @@
         // their lower bound and exclude their higher bound so we need to have the first step
         // start at exactly 1 unit higher than floor(unigramFreq + half a step).
         // Note : to reconstruct the score, the dictionary reader will need to divide
-        // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise, and add
-        // (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.
+        // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step,
+        // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best
+        // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the
+        // step pointed by the discretized frequency.
         final float stepSize =
                 (MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5f + MAX_BIGRAM_FREQUENCY);
         final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f);
@@ -1091,46 +1093,46 @@
     // readDictionaryBinary is the public entry point for them.
 
     static final int[] characterBuffer = new int[MAX_WORD_LENGTH];
-    private static CharGroupInfo readCharGroup(RandomAccessFile source,
-            final int originalGroupAddress) throws IOException {
+    private static CharGroupInfo readCharGroup(final ByteBuffer buffer,
+            final int originalGroupAddress) {
         int addressPointer = originalGroupAddress;
-        final int flags = source.readUnsignedByte();
+        final int flags = readUnsignedByte(buffer);
         ++addressPointer;
         final int characters[];
         if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
             int index = 0;
-            int character = CharEncoding.readChar(source);
+            int character = CharEncoding.readChar(buffer);
             addressPointer += CharEncoding.getCharSize(character);
             while (-1 != character) {
                 characterBuffer[index++] = character;
-                character = CharEncoding.readChar(source);
+                character = CharEncoding.readChar(buffer);
                 addressPointer += CharEncoding.getCharSize(character);
             }
             characters = Arrays.copyOfRange(characterBuffer, 0, index);
         } else {
-            final int character = CharEncoding.readChar(source);
+            final int character = CharEncoding.readChar(buffer);
             addressPointer += CharEncoding.getCharSize(character);
             characters = new int[] { character };
         }
         final int frequency;
         if (0 != (FLAG_IS_TERMINAL & flags)) {
             ++addressPointer;
-            frequency = source.readUnsignedByte();
+            frequency = readUnsignedByte(buffer);
         } else {
             frequency = CharGroup.NOT_A_TERMINAL;
         }
         int childrenAddress = addressPointer;
         switch (flags & MASK_GROUP_ADDRESS_TYPE) {
         case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
-            childrenAddress += source.readUnsignedByte();
+            childrenAddress += readUnsignedByte(buffer);
             addressPointer += 1;
             break;
         case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
-            childrenAddress += source.readUnsignedShort();
+            childrenAddress += readUnsignedShort(buffer);
             addressPointer += 2;
             break;
         case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
-            childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
+            childrenAddress += readUnsignedInt24(buffer);
             addressPointer += 3;
             break;
         case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
@@ -1140,38 +1142,38 @@
         }
         ArrayList<WeightedString> shortcutTargets = null;
         if (0 != (flags & FLAG_HAS_SHORTCUT_TARGETS)) {
-            final long pointerBefore = source.getFilePointer();
+            final int pointerBefore = buffer.position();
             shortcutTargets = new ArrayList<WeightedString>();
-            source.readUnsignedShort(); // Skip the size
+            buffer.getShort(); // Skip the size
             while (true) {
-                final int targetFlags = source.readUnsignedByte();
-                final String word = CharEncoding.readString(source);
+                final int targetFlags = readUnsignedByte(buffer);
+                final String word = CharEncoding.readString(buffer);
                 shortcutTargets.add(new WeightedString(word,
                         targetFlags & FLAG_ATTRIBUTE_FREQUENCY));
                 if (0 == (targetFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
             }
-            addressPointer += (source.getFilePointer() - pointerBefore);
+            addressPointer += buffer.position() - pointerBefore;
         }
         ArrayList<PendingAttribute> bigrams = null;
         if (0 != (flags & FLAG_HAS_BIGRAMS)) {
             bigrams = new ArrayList<PendingAttribute>();
             while (true) {
-                final int bigramFlags = source.readUnsignedByte();
+                final int bigramFlags = readUnsignedByte(buffer);
                 ++addressPointer;
                 final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
                 int bigramAddress = addressPointer;
                 switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
                 case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
-                    bigramAddress += sign * source.readUnsignedByte();
+                    bigramAddress += sign * readUnsignedByte(buffer);
                     addressPointer += 1;
                     break;
                 case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
-                    bigramAddress += sign * source.readUnsignedShort();
+                    bigramAddress += sign * readUnsignedShort(buffer);
                     addressPointer += 2;
                     break;
                 case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
-                    final int offset = ((source.readUnsignedByte() << 16)
-                            + source.readUnsignedShort());
+                    final int offset = (readUnsignedByte(buffer) << 16)
+                            + readUnsignedShort(buffer);
                     bigramAddress += sign * offset;
                     addressPointer += 3;
                     break;
@@ -1188,15 +1190,15 @@
     }
 
     /**
-     * Reads and returns the char group count out of a file and forwards the pointer.
+     * Reads and returns the char group count out of a buffer and forwards the pointer.
      */
-    private static int readCharGroupCount(RandomAccessFile source) throws IOException {
-        final int msb = source.readUnsignedByte();
+    private static int readCharGroupCount(final ByteBuffer buffer) {
+        final int msb = readUnsignedByte(buffer);
         if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
             return msb;
         } else {
             return ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
-                    + source.readUnsignedByte();
+                    + readUnsignedByte(buffer);
         }
     }
 
@@ -1204,31 +1206,29 @@
     // of this method. Since it performs direct, unbuffered random access to the file and
     // may be called hundreds of thousands of times, the resulting performance is not
     // reasonable without some kind of cache. Thus:
-    // TODO: perform buffered I/O here and in other places in the code.
     private static TreeMap<Integer, String> wordCache = new TreeMap<Integer, String>();
     /**
      * Finds, as a string, the word at the address passed as an argument.
      *
-     * @param source the file to read from.
+     * @param buffer the buffer to read from.
      * @param headerSize the size of the header.
      * @param address the address to seek.
      * @return the word, as a string.
-     * @throws IOException if the file can't be read.
      */
-    private static String getWordAtAddress(final RandomAccessFile source, final long headerSize,
-            int address) throws IOException {
+    private static String getWordAtAddress(final ByteBuffer buffer, final int headerSize,
+            final int address) {
         final String cachedString = wordCache.get(address);
         if (null != cachedString) return cachedString;
-        final long originalPointer = source.getFilePointer();
-        source.seek(headerSize);
-        final int count = readCharGroupCount(source);
+        final int originalPointer = buffer.position();
+        buffer.position(headerSize);
+        final int count = readCharGroupCount(buffer);
         int groupOffset = getGroupCountSize(count);
         final StringBuilder builder = new StringBuilder();
         String result = null;
 
         CharGroupInfo last = null;
         for (int i = count - 1; i >= 0; --i) {
-            CharGroupInfo info = readCharGroup(source, groupOffset);
+            CharGroupInfo info = readCharGroup(buffer, groupOffset);
             groupOffset = info.mEndAddress;
             if (info.mOriginalAddress == address) {
                 builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
@@ -1239,9 +1239,9 @@
                 if (info.mChildrenAddress > address) {
                     if (null == last) continue;
                     builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
-                    source.seek(last.mChildrenAddress + headerSize);
+                    buffer.position(last.mChildrenAddress + headerSize);
                     groupOffset = last.mChildrenAddress + 1;
-                    i = source.readUnsignedByte();
+                    i = readUnsignedByte(buffer);
                     last = null;
                     continue;
                 }
@@ -1249,14 +1249,14 @@
             }
             if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
                 builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
-                source.seek(last.mChildrenAddress + headerSize);
+                buffer.position(last.mChildrenAddress + headerSize);
                 groupOffset = last.mChildrenAddress + 1;
-                i = source.readUnsignedByte();
+                i = readUnsignedByte(buffer);
                 last = null;
                 continue;
             }
         }
-        source.seek(originalPointer);
+        buffer.position(originalPointer);
         wordCache.put(address, result);
         return result;
     }
@@ -1269,44 +1269,47 @@
      * This will recursively read other nodes into the structure, populating the reverse
      * maps on the fly and using them to keep track of already read nodes.
      *
-     * @param source the data file, correctly positioned at the start of a node.
+     * @param buffer the buffer, correctly positioned at the start of a node.
      * @param headerSize the size, in bytes, of the file header.
      * @param reverseNodeMap a mapping from addresses to already read nodes.
      * @param reverseGroupMap a mapping from addresses to already read character groups.
      * @return the read node with all his children already read.
      */
-    private static Node readNode(RandomAccessFile source, long headerSize,
-            Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap)
+    private static Node readNode(final ByteBuffer buffer, final int headerSize,
+            final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap)
             throws IOException {
-        final int nodeOrigin = (int)(source.getFilePointer() - headerSize);
-        final int count = readCharGroupCount(source);
+        final int nodeOrigin = buffer.position() - headerSize;
+        final int count = readCharGroupCount(buffer);
         final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
         int groupOffset = nodeOrigin + getGroupCountSize(count);
         for (int i = count; i > 0; --i) {
-            CharGroupInfo info = readCharGroup(source, groupOffset);
+            CharGroupInfo info =readCharGroup(buffer, groupOffset);
             ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
             ArrayList<WeightedString> bigrams = null;
             if (null != info.mBigrams) {
                 bigrams = new ArrayList<WeightedString>();
                 for (PendingAttribute bigram : info.mBigrams) {
-                    final String word = getWordAtAddress(source, headerSize, bigram.mAddress);
+                    final String word = getWordAtAddress(
+                            buffer, headerSize, bigram.mAddress);
                     bigrams.add(new WeightedString(word, bigram.mFrequency));
                 }
             }
             if (hasChildrenAddress(info.mChildrenAddress)) {
                 Node children = reverseNodeMap.get(info.mChildrenAddress);
                 if (null == children) {
-                    final long currentPosition = source.getFilePointer();
-                    source.seek(info.mChildrenAddress + headerSize);
-                    children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap);
-                    source.seek(currentPosition);
+                    final int currentPosition = buffer.position();
+                    buffer.position(info.mChildrenAddress + headerSize);
+                    children = readNode(
+                            buffer, headerSize, reverseNodeMap, reverseGroupMap);
+                    buffer.position(currentPosition);
                 }
                 nodeContents.add(
-                        new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency,
-                                children));
+                        new CharGroup(info.mCharacters, shortcutTargets,
+                                bigrams, info.mFrequency, children));
             } else {
                 nodeContents.add(
-                        new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency));
+                        new CharGroup(info.mCharacters, shortcutTargets,
+                                bigrams, info.mFrequency));
             }
             groupOffset = info.mEndAddress;
         }
@@ -1318,57 +1321,76 @@
 
     /**
      * Helper function to get the binary format version from the header.
+     * @throws IOException
      */
-    private static int getFormatVersion(final RandomAccessFile source) throws IOException {
-        final int magic_v1 = source.readUnsignedShort();
-        if (VERSION_1_MAGIC_NUMBER == magic_v1) return source.readUnsignedByte();
-        final int magic_v2 = (magic_v1 << 16) + source.readUnsignedShort();
-        if (VERSION_2_MAGIC_NUMBER == magic_v2) return source.readUnsignedShort();
+    private static int getFormatVersion(final ByteBuffer buffer) throws IOException {
+        final int magic_v1 = readUnsignedShort(buffer);
+        if (VERSION_1_MAGIC_NUMBER == magic_v1) return readUnsignedByte(buffer);
+        final int magic_v2 = (magic_v1 << 16) + readUnsignedShort(buffer);
+        if (VERSION_2_MAGIC_NUMBER == magic_v2) return readUnsignedShort(buffer);
         return NOT_A_VERSION_NUMBER;
     }
 
     /**
-     * Reads a random access file and returns the memory representation of the dictionary.
+     * Reads options from a file and populate a map with their contents.
+     *
+     * The file is read at the current file pointer, so the caller must take care the pointer
+     * is in the right place before calling this.
+     */
+    public static void populateOptions(final ByteBuffer buffer, final int headerSize,
+            final HashMap<String, String> options) {
+        while (buffer.position() < headerSize) {
+            final String key = CharEncoding.readString(buffer);
+            final String value = CharEncoding.readString(buffer);
+            options.put(key, value);
+        }
+    }
+
+    /**
+     * Reads a byte buffer and returns the memory representation of the dictionary.
      *
      * This high-level method takes a binary file and reads its contents, populating a
      * FusionDictionary structure. The optional dict argument is an existing dictionary to
      * which words from the file should be added. If it is null, a new dictionary is created.
      *
-     * @param source the file to read.
+     * @param buffer the buffer to read.
      * @param dict an optional dictionary to add words to, or null.
      * @return the created (or merged) dictionary.
      */
-    public static FusionDictionary readDictionaryBinary(final RandomAccessFile source,
+    public static FusionDictionary readDictionaryBinary(final ByteBuffer buffer,
             final FusionDictionary dict) throws IOException, UnsupportedFormatException {
         // Check file version
-        final int version = getFormatVersion(source);
-        if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION ) {
+        final int version = getFormatVersion(buffer);
+        if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) {
             throw new UnsupportedFormatException("This file has version " + version
                     + ", but this implementation does not support versions above "
                     + MAXIMUM_SUPPORTED_VERSION);
         }
 
-        // Read options
-        final int optionsFlags = source.readUnsignedShort();
+        // clear cache
+        wordCache.clear();
 
-        final long headerSize;
+        // Read options
+        final int optionsFlags = readUnsignedShort(buffer);
+
+        final int headerSize;
         final HashMap<String, String> options = new HashMap<String, String>();
         if (version < FIRST_VERSION_WITH_HEADER_SIZE) {
-            headerSize = source.getFilePointer();
+            headerSize = buffer.position();
         } else {
-            headerSize = (source.readUnsignedByte() << 24) + (source.readUnsignedByte() << 16)
-                    + (source.readUnsignedByte() << 8) + source.readUnsignedByte();
-            while (source.getFilePointer() < headerSize) {
-                final String key = CharEncoding.readString(source);
-                final String value = CharEncoding.readString(source);
-                options.put(key, value);
-            }
-            source.seek(headerSize);
+            headerSize = buffer.getInt();
+            populateOptions(buffer, headerSize, options);
+            buffer.position(headerSize);
+        }
+
+        if (headerSize < 0) {
+            throw new UnsupportedFormatException("header size can't be negative.");
         }
 
         Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
         Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
-        final Node root = readNode(source, headerSize, reverseNodeMapping, reverseGroupMapping);
+        final Node root = readNode(
+                buffer, headerSize, reverseNodeMapping, reverseGroupMapping);
 
         FusionDictionary newDict = new FusionDictionary(root,
                 new FusionDictionary.DictionaryOptions(options,
@@ -1392,6 +1414,28 @@
     }
 
     /**
+     * Helper function to read one byte from ByteBuffer.
+     */
+    private static int readUnsignedByte(final ByteBuffer buffer) {
+        return ((int)buffer.get()) & 0xFF;
+    }
+
+    /**
+     * Helper function to read two byte from ByteBuffer.
+     */
+    private static int readUnsignedShort(final ByteBuffer buffer) {
+        return ((int)buffer.getShort()) & 0xFFFF;
+    }
+
+    /**
+     * Helper function to read three byte from ByteBuffer.
+     */
+    private static int readUnsignedInt24(final ByteBuffer buffer) {
+        final int value = readUnsignedByte(buffer) << 16;
+        return value + readUnsignedShort(buffer);
+    }
+
+    /**
      * Basic test to find out whether the file is a binary dictionary or not.
      *
      * Concretely this only tests the magic number.
@@ -1400,14 +1444,44 @@
      * @return true if it's a binary dictionary, false otherwise
      */
     public static boolean isBinaryDictionary(final String filename) {
+        FileInputStream inStream = null;
         try {
-            RandomAccessFile f = new RandomAccessFile(filename, "r");
-            final int version = getFormatVersion(f);
+            final File file = new File(filename);
+            inStream = new FileInputStream(file);
+            final ByteBuffer buffer = inStream.getChannel().map(
+                    FileChannel.MapMode.READ_ONLY, 0, file.length());
+            final int version = getFormatVersion(buffer);
             return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION);
         } catch (FileNotFoundException e) {
             return false;
         } catch (IOException e) {
             return false;
+        } finally {
+            if (inStream != null) {
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    // do nothing
+                }
+            }
         }
     }
+
+    /**
+     * Calculate bigram frequency from compressed value
+     *
+     * @see #makeBigramFlags
+     *
+     * @param unigramFrequency
+     * @param bigramFrequency compressed frequency
+     * @return approximate bigram frequency
+     */
+    public static int reconstructBigramFrequency(final int unigramFrequency,
+            final int bigramFrequency) {
+        final float stepSize = (MAX_TERMINAL_FREQUENCY - unigramFrequency)
+                / (1.5f + MAX_BIGRAM_FREQUENCY);
+        final float resultFreqFloat = (float)unigramFrequency
+                + stepSize * (bigramFrequency + 1.0f);
+        return (int)resultFreqFloat;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 5864db2..7c15ba5 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -516,13 +516,23 @@
             int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
             if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
             currentGroup = node.mData.get(indexOfGroup);
+
+            if (s.length() - index < currentGroup.mChars.length) return null;
+            int newIndex = index;
+            while (newIndex < s.length() && newIndex - index < currentGroup.mChars.length) {
+                if (currentGroup.mChars[newIndex - index] != s.codePointAt(newIndex)) return null;
+                newIndex++;
+            }
+            index = newIndex;
+
             if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
-            index += currentGroup.mChars.length;
             if (index < s.length()) {
                 node = currentGroup.mChildren;
             }
         } while (null != node && index < s.length());
 
+        if (index < s.length()) return null;
+        if (!currentGroup.isTerminal()) return null;
         if (DBG && !s.equals(checker.toString())) return null;
         return currentGroup;
     }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 3bdfe1f..eef7a51 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -25,6 +25,7 @@
 
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.ContactsBinaryDictionary;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.DictionaryCollection;
@@ -35,7 +36,6 @@
 import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary;
 import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
 import com.android.inputmethod.latin.UserBinaryDictionary;
-import com.android.inputmethod.latin.WhitelistDictionary;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -63,12 +63,9 @@
     public static final int CAPITALIZE_ALL = 2; // All caps
 
     private final static String[] EMPTY_STRING_ARRAY = new String[0];
-    private Map<String, DictionaryPool> mDictionaryPools =
-            Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
+    private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
     private Map<String, UserBinaryDictionary> mUserDictionaries =
-            Collections.synchronizedMap(new TreeMap<String, UserBinaryDictionary>());
-    private Map<String, Dictionary> mWhitelistDictionaries =
-            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+            CollectionUtils.newSynchronizedTreeMap();
     private ContactsBinaryDictionary mContactsDictionary;
 
     // The threshold for a candidate to be offered as a suggestion.
@@ -80,7 +77,7 @@
     private final Object mUseContactsLock = new Object();
 
     private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
-            new HashSet<WeakReference<DictionaryCollection>>();
+            CollectionUtils.newHashSet();
 
     public static final int SCRIPT_LATIN = 0;
     public static final int SCRIPT_CYRILLIC = 1;
@@ -96,7 +93,7 @@
         // proximity to pass to the dictionary descent algorithm.
         // IMPORTANT: this only contains languages - do not write countries in there.
         // Only the language is searched from the map.
-        mLanguageToScript = new TreeMap<String, Integer>();
+        mLanguageToScript = CollectionUtils.newTreeMap();
         mLanguageToScript.put("en", SCRIPT_LATIN);
         mLanguageToScript.put("fr", SCRIPT_LATIN);
         mLanguageToScript.put("de", SCRIPT_LATIN);
@@ -234,7 +231,7 @@
             mSuggestionThreshold = suggestionThreshold;
             mRecommendedThreshold = recommendedThreshold;
             mMaxLength = maxLength;
-            mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
+            mSuggestions = CollectionUtils.newArrayList(maxLength + 1);
             mScores = new int[mMaxLength];
         }
 
@@ -362,12 +359,9 @@
 
     private void closeAllDictionaries() {
         final Map<String, DictionaryPool> oldPools = mDictionaryPools;
-        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
+        mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
         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>());
+        mUserDictionaries = CollectionUtils.newSynchronizedTreeMap();
         new Thread("spellchecker_close_dicts") {
             @Override
             public void run() {
@@ -377,9 +371,6 @@
                 for (Dictionary dict : oldUserDictionaries.values()) {
                     dict.close();
                 }
-                for (Dictionary dict : oldWhitelistDictionaries.values()) {
-                    dict.close();
-                }
                 synchronized (mUseContactsLock) {
                     if (null != mContactsDictionary) {
                         // The synchronously loaded contacts dictionary should have been in one
@@ -423,12 +414,6 @@
             mUserDictionaries.put(localeStr, userDictionary);
         }
         dictionaryCollection.addDictionary(userDictionary);
-        Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
-        if (null == whitelistDictionary) {
-            whitelistDictionary = new WhitelistDictionary(this, locale);
-            mWhitelistDictionaries.put(localeStr, whitelistDictionary);
-        }
-        dictionaryCollection.addDictionary(whitelistDictionary);
         synchronized (mUseContactsLock) {
             if (mUseContactsDictionary) {
                 if (null == mContactsDictionary) {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
index 501a0e2..5a1bd37 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -22,6 +22,8 @@
 import android.view.textservice.SuggestionsInfo;
 import android.view.textservice.TextInfo;
 
+import com.android.inputmethod.latin.CollectionUtils;
+
 import java.util.ArrayList;
 
 public class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession {
@@ -40,10 +42,10 @@
             return null;
         }
         final int N = ssi.getSuggestionsCount();
-        final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>();
-        final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
+        final ArrayList<Integer> additionalOffsets = CollectionUtils.newArrayList();
+        final ArrayList<Integer> additionalLengths = CollectionUtils.newArrayList();
         final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
-                new ArrayList<SuggestionsInfo>();
+                CollectionUtils.newArrayList();
         String currentWord = null;
         for (int i = 0; i < N; ++i) {
             final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 0171dc0..f4784ff 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -24,6 +24,7 @@
 import android.view.textservice.TextInfo;
 
 import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.LocaleUtils;
 import com.android.inputmethod.latin.WordComposer;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -193,8 +194,8 @@
             if (shouldFilterOut(inText, mScript)) {
                 DictAndProximity dictInfo = null;
                 try {
-                    dictInfo = mDictionaryPool.takeOrGetNull();
-                    if (null == dictInfo) {
+                    dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+                    if (!DictionaryPool.isAValidDictionary(dictInfo)) {
                         return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
                     }
                     return dictInfo.mDictionary.isValidWord(inText)
@@ -225,8 +226,8 @@
                 final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
                         codePoint, mScript);
                 if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
-                    composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
-                            WordComposer.NOT_A_COORDINATE);
+                    composer.add(codePoint,
+                            Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
                 } else {
                     composer.add(codePoint, xy & 0xFFFF, xy >> 16);
                 }
@@ -236,8 +237,8 @@
             boolean isInDict = true;
             DictAndProximity dictInfo = null;
             try {
-                dictInfo = mDictionaryPool.takeOrGetNull();
-                if (null == dictInfo) {
+                dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+                if (!DictionaryPool.isAValidDictionary(dictInfo)) {
                     return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
                 }
                 final ArrayList<SuggestedWordInfo> suggestions =
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index 8fc632e..53aa6c7 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -16,19 +16,56 @@
 
 package com.android.inputmethod.latin.spellcheck;
 
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.WordComposer;
+
+import java.util.ArrayList;
 import java.util.Locale;
 import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A blocking queue that creates dictionaries up to a certain limit as necessary.
+ * As a deadlock-detecting device, if waiting for more than TIMEOUT = 3 seconds, we
+ * will clear the queue and generate its contents again. This is transparent for
+ * the client code, but may help with sloppy clients.
  */
 @SuppressWarnings("serial")
 public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
+    private final static String TAG = DictionaryPool.class.getSimpleName();
+    // How many seconds we wait for a dictionary to become available. Past this delay, we give up in
+    // fear some bug caused a deadlock, and reset the whole pool.
+    private final static int TIMEOUT = 3;
     private final AndroidSpellCheckerService mService;
     private final int mMaxSize;
     private final Locale mLocale;
     private int mSize;
     private volatile boolean mClosed;
+    final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList();
+    private final static DictAndProximity dummyDict = new DictAndProximity(
+            new Dictionary(Dictionary.TYPE_MAIN) {
+                @Override
+                public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+                        final CharSequence prevWord, final ProximityInfo proximityInfo) {
+                    return noSuggestions;
+                }
+                @Override
+                public boolean isValidWord(CharSequence word) {
+                    // This is never called. However if for some strange reason it ever gets
+                    // called, returning true is less destructive (it will not underline the
+                    // word in red).
+                    return true;
+                }
+            }, null);
+
+    static public boolean isAValidDictionary(final DictAndProximity dictInfo) {
+        return null != dictInfo && dummyDict != dictInfo;
+    }
 
     public DictionaryPool(final int maxSize, final AndroidSpellCheckerService service,
             final Locale locale) {
@@ -41,13 +78,23 @@
     }
 
     @Override
-    public DictAndProximity take() throws InterruptedException {
+    public DictAndProximity poll(final long timeout, final TimeUnit unit)
+            throws InterruptedException {
         final DictAndProximity dict = poll();
         if (null != dict) return dict;
         synchronized(this) {
             if (mSize >= mMaxSize) {
-                // Our pool is already full. Wait until some dictionary is ready.
-                return super.take();
+                // Our pool is already full. Wait until some dictionary is ready, or TIMEOUT
+                // expires to avoid a deadlock.
+                final DictAndProximity result = super.poll(timeout, unit);
+                if (null == result) {
+                    Log.e(TAG, "Deadlock detected ! Resetting dictionary pool");
+                    clear();
+                    mSize = 1;
+                    return mService.createDictAndProximity(mLocale);
+                } else {
+                    return result;
+                }
             } else {
                 ++mSize;
                 return mService.createDictAndProximity(mLocale);
@@ -56,9 +103,9 @@
     }
 
     // Convenience method
-    public DictAndProximity takeOrGetNull() {
+    public DictAndProximity pollWithDefaultTimeout() {
         try {
-            return take();
+            return poll(TIMEOUT, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
             return null;
         }
@@ -78,7 +125,7 @@
     public boolean offer(final DictAndProximity dict) {
         if (mClosed) {
             dict.mDictionary.close();
-            return false;
+            return super.offer(dummyDict);
         } else {
             return super.offer(dict);
         }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
index 0103e84..fe5225e 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
@@ -16,14 +16,15 @@
 
 package com.android.inputmethod.latin.spellcheck;
 
-import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
 
 import java.util.TreeMap;
 
 public class SpellCheckerProximityInfo {
     /* public for test */
-    final public static int NUL = KeyDetector.NOT_A_CODE;
+    final public static int NUL = Constants.NOT_A_CODE;
 
     // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside
     // native code - this value is passed at creation of the binary object and reused
@@ -59,7 +60,7 @@
         // character.
         // Since we need to build such an array, we want to be able to search in our big proximity
         // data quickly by character, and a map is probably the best way to do this.
-        final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+        final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap();
 
         // The proximity here is the union of
         // - the proximity for a QWERTY keyboard.
@@ -111,6 +112,7 @@
             NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
             NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
             NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
         };
         static {
             buildProximityIndices(PROXIMITY, INDICES);
@@ -121,7 +123,7 @@
     }
 
     private static class Cyrillic {
-        final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+        final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap();
         // TODO: The following table is solely based on the keyboard layout. Consult with Russian
         // speakers on commonly misspelled words/letters.
         final static int[] PROXIMITY = {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index b57ffd2..03263d2 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -58,6 +58,7 @@
 import com.android.inputmethod.keyboard.PointerTracker;
 import com.android.inputmethod.keyboard.ViewLayoutUtils;
 import com.android.inputmethod.latin.AutoCorrection;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
@@ -72,7 +73,7 @@
         OnLongClickListener {
     public interface Listener {
         public boolean addWordToUserDictionary(String word);
-        public void pickSuggestionManually(int index, CharSequence word, int x, int y);
+        public void pickSuggestionManually(int index, CharSequence word);
     }
 
     // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
@@ -88,9 +89,9 @@
     private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
     private final PopupWindow mMoreSuggestionsWindow;
 
-    private final ArrayList<TextView> mWords = new ArrayList<TextView>();
-    private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
-    private final ArrayList<View> mDividers = new ArrayList<View>();
+    private final ArrayList<TextView> mWords = CollectionUtils.newArrayList();
+    private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList();
+    private final ArrayList<View> mDividers = CollectionUtils.newArrayList();
 
     private final PopupWindow mPreviewPopup;
     private final TextView mPreviewText;
@@ -131,7 +132,7 @@
 
     private static class SuggestionStripViewParams {
         private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
-        private static final int DEFAULT_CENTER_SUGGESTION_PERCENTILE = 40;
+        private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
         private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
         private static final int PUNCTUATIONS_IN_STRIP = 5;
 
@@ -167,7 +168,7 @@
 
         private final int mSuggestionStripOption;
 
-        private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
+        private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList();
 
         public boolean mMoreSuggestionsAvailable;
 
@@ -195,16 +196,16 @@
                     R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
             mSuggestionStripOption = a.getInt(
                     R.styleable.SuggestionStripView_suggestionStripOption, 0);
-            final float alphaValidTypedWord = getPercent(a,
-                    R.styleable.SuggestionStripView_alphaValidTypedWord, 100);
-            final float alphaTypedWord = getPercent(a,
-                    R.styleable.SuggestionStripView_alphaTypedWord, 100);
-            final float alphaAutoCorrect = getPercent(a,
-                    R.styleable.SuggestionStripView_alphaAutoCorrect, 100);
-            final float alphaSuggested = getPercent(a,
-                    R.styleable.SuggestionStripView_alphaSuggested, 100);
-            mAlphaObsoleted = getPercent(a,
-                    R.styleable.SuggestionStripView_alphaSuggested, 100);
+            final float alphaValidTypedWord = getFraction(a,
+                    R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
+            final float alphaTypedWord = getFraction(a,
+                    R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
+            final float alphaAutoCorrect = getFraction(a,
+                    R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
+            final float alphaSuggested = getFraction(a,
+                    R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
+            mAlphaObsoleted = getFraction(a,
+                    R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
             mColorValidTypedWord = applyAlpha(a.getColor(
                     R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
             mColorTypedWord = applyAlpha(a.getColor(
@@ -216,14 +217,14 @@
             mSuggestionsCountInStrip = a.getInt(
                     R.styleable.SuggestionStripView_suggestionsCountInStrip,
                     DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
-            mCenterSuggestionWeight = getPercent(a,
+            mCenterSuggestionWeight = getFraction(a,
                     R.styleable.SuggestionStripView_centerSuggestionPercentile,
                     DEFAULT_CENTER_SUGGESTION_PERCENTILE);
             mMaxMoreSuggestionsRow = a.getInt(
                     R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
                     DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
-            mMinMoreSuggestionsWidth = getRatio(a,
-                    R.styleable.SuggestionStripView_minMoreSuggestionsWidth);
+            mMinMoreSuggestionsWidth = getFraction(a,
+                    R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
             a.recycle();
 
             mMoreSuggestionsHint = getMoreSuggestionsHint(res,
@@ -277,14 +278,8 @@
             return new BitmapDrawable(res, buffer);
         }
 
-        // Read integer value in TypedArray as percent.
-        private static float getPercent(TypedArray a, int index, int defValue) {
-            return a.getInt(index, defValue) / 100.0f;
-        }
-
-        // Read fraction value in TypedArray as float.
-        private static float getRatio(TypedArray a, int index) {
-            return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
+        static float getFraction(final TypedArray a, final int index, final float defValue) {
+            return a.getFraction(index, 1, 1, defValue);
         }
 
         private CharSequence getStyledSuggestionWord(SuggestedWords suggestedWords, int pos) {
@@ -726,9 +721,7 @@
         public boolean onCustomRequest(int requestCode) {
             final int index = requestCode;
             final CharSequence word = mSuggestedWords.getWord(index);
-            // TODO: change caller path so coordinates are passed through here
-            mListener.pickSuggestionManually(index, word, NOT_A_TOUCH_COORDINATE,
-                    NOT_A_TOUCH_COORDINATE);
+            mListener.pickSuggestionManually(index, word);
             dismissMoreSuggestions();
             return true;
         }
@@ -874,7 +867,7 @@
             return;
 
         final CharSequence word = mSuggestedWords.getWord(index);
-        mListener.pickSuggestionManually(index, word, mLastX, mLastY);
+        mListener.pickSuggestionManually(index, word);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
new file mode 100644
index 0000000..5124a35
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
@@ -0,0 +1,33 @@
+/*
+ * 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.research;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Arrange for the uploading service to be run on regular intervals.
+ */
+public final class BootBroadcastReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
+            ResearchLogger.scheduleUploadingService(context);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java
index c9f3b47..11eae88 100644
--- a/java/src/com/android/inputmethod/research/FeedbackActivity.java
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -18,10 +18,7 @@
 
 import android.app.Activity;
 import android.os.Bundle;
-import android.text.Editable;
-import android.view.View;
 import android.widget.CheckBox;
-import android.widget.EditText;
 
 import com.android.inputmethod.latin.R;
 
@@ -31,6 +28,11 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.research_feedback_activity);
         final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout);
+        final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history);
+        final CharSequence cs = checkbox.getText();
+        final String actualString = String.format(cs.toString(),
+                ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE);
+        checkbox.setText(actualString);
         layout.setActivity(this);
     }
 
diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java
new file mode 100644
index 0000000..ae7b157
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LogBuffer.java
@@ -0,0 +1,113 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.LinkedList;
+
+/**
+ * A buffer that holds a fixed number of LogUnits.
+ *
+ * LogUnits are added in and shifted out in temporal order.  Only a subset of the LogUnits are
+ * actual words; the other LogUnits do not count toward the word limit.  Once the buffer reaches
+ * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to
+ * stay under the capacity limit.
+ */
+public class LogBuffer {
+    protected final LinkedList<LogUnit> mLogUnits;
+    /* package for test */ int mWordCapacity;
+    // The number of members of mLogUnits that are actual words.
+    protected int mNumActualWords;
+
+    /**
+     * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and
+     * unlimited number of non-word LogUnits), and that outputs its result to a researchLog.
+     *
+     * @param wordCapacity maximum number of words
+     */
+    LogBuffer(final int wordCapacity) {
+        if (wordCapacity <= 0) {
+            throw new IllegalArgumentException("wordCapacity must be 1 or greater.");
+        }
+        mLogUnits = CollectionUtils.newLinkedList();
+        mWordCapacity = wordCapacity;
+        mNumActualWords = 0;
+    }
+
+    /**
+     * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
+     * (oldest first) if word capacity is reached.
+     */
+    public void shiftIn(LogUnit newLogUnit) {
+        if (newLogUnit.getWord() == null) {
+            // This LogUnit isn't a word, so it doesn't count toward the word-limit.
+            mLogUnits.add(newLogUnit);
+            return;
+        }
+        if (mNumActualWords == mWordCapacity) {
+            shiftOutThroughFirstWord();
+        }
+        mLogUnits.add(newLogUnit);
+        mNumActualWords++; // Must be a word, or we wouldn't be here.
+    }
+
+    private void shiftOutThroughFirstWord() {
+        while (!mLogUnits.isEmpty()) {
+            final LogUnit logUnit = mLogUnits.removeFirst();
+            onShiftOut(logUnit);
+            if (logUnit.hasWord()) {
+                // Successfully shifted out a word-containing LogUnit and made space for the new
+                // LogUnit.
+                mNumActualWords--;
+                break;
+            }
+        }
+    }
+
+    /**
+     * Removes all LogUnits from the buffer without calling onShiftOut().
+     */
+    public void clear() {
+        mLogUnits.clear();
+        mNumActualWords = 0;
+    }
+
+    /**
+     * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn.  LogUnits are
+     * removed in the order entered.  This method is not called when shiftOut is called directly.
+     *
+     * Base class does nothing; subclasses may override.
+     */
+    protected void onShiftOut(LogUnit logUnit) {
+    }
+
+    /**
+     * Called to deliberately remove the oldest LogUnit.  Usually called when draining the
+     * LogBuffer.
+     */
+    public LogUnit shiftOut() {
+        if (mLogUnits.isEmpty()) {
+            return null;
+        }
+        final LogUnit logUnit = mLogUnits.removeFirst();
+        if (logUnit.hasWord()) {
+            mNumActualWords--;
+        }
+        return logUnit;
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
new file mode 100644
index 0000000..d8b3a29
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -0,0 +1,83 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.ArrayList;
+
+/**
+ * A group of log statements related to each other.
+ *
+ * A LogUnit is collection of LogStatements, each of which is generated by at a particular point
+ * in the code.  (There is no LogStatement class; the data is stored across the instance variables
+ * here.)  A single LogUnit's statements can correspond to all the calls made while in the same
+ * composing region, or all the calls between committing the last composing region, and the first
+ * character of the next composing region.
+ *
+ * Individual statements in a log may be marked as potentially private.  If so, then they are only
+ * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit
+ * will not violate the user's privacy.  Checks for this may include whether other LogUnits have
+ * been published recently, or whether the LogUnit contains numbers, etc.
+ */
+/* package */ class LogUnit {
+    private final ArrayList<String[]> mKeysList = CollectionUtils.newArrayList();
+    private final ArrayList<Object[]> mValuesList = CollectionUtils.newArrayList();
+    private final ArrayList<Boolean> mIsPotentiallyPrivate = CollectionUtils.newArrayList();
+    private String mWord;
+    private boolean mContainsDigit;
+
+    public void addLogStatement(final String[] keys, final Object[] values,
+            final Boolean isPotentiallyPrivate) {
+        mKeysList.add(keys);
+        mValuesList.add(values);
+        mIsPotentiallyPrivate.add(isPotentiallyPrivate);
+    }
+
+    public void publishTo(final ResearchLog researchLog, final boolean isIncludingPrivateData) {
+        final int size = mKeysList.size();
+        for (int i = 0; i < size; i++) {
+            if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) {
+                researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i));
+            }
+        }
+    }
+
+    public void setWord(String word) {
+        mWord = word;
+    }
+
+    public String getWord() {
+        return mWord;
+    }
+
+    public boolean hasWord() {
+        return mWord != null;
+    }
+
+    public void setContainsDigit() {
+        mContainsDigit = true;
+    }
+
+    public boolean hasDigit() {
+        return mContainsDigit;
+    }
+
+    public boolean isEmpty() {
+        return mKeysList.isEmpty();
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
new file mode 100644
index 0000000..745768d
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -0,0 +1,127 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.Suggest;
+
+import java.util.Random;
+
+public class MainLogBuffer extends LogBuffer {
+    // The size of the n-grams logged.  E.g. N_GRAM_SIZE = 2 means to sample bigrams.
+    private static final int N_GRAM_SIZE = 2;
+    // The number of words between n-grams to omit from the log.
+    private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18;
+
+    private final ResearchLog mResearchLog;
+    private Suggest mSuggest;
+
+    // The minimum periodicity with which n-grams can be sampled.  E.g. mWinWordPeriod is 10 if
+    // every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc.
+    // for 11-18, and the bigram at words 19 and 20.  If an n-gram is not safe (e.g. it  contains a
+    // number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe
+    // n-gram does appear.
+    /* package for test */ int mMinWordPeriod;
+
+    // Counter for words left to suppress before an n-gram can be sampled.  Reset to mMinWordPeriod
+    // after a sample is taken.
+    /* package for test */ int mWordsUntilSafeToSample;
+
+    public MainLogBuffer(final ResearchLog researchLog) {
+        super(N_GRAM_SIZE);
+        mResearchLog = researchLog;
+        mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE;
+        final Random random = new Random();
+        mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod);
+    }
+
+    public void setSuggest(Suggest suggest) {
+        mSuggest = suggest;
+    }
+
+    @Override
+    public void shiftIn(final LogUnit newLogUnit) {
+        super.shiftIn(newLogUnit);
+        if (newLogUnit.hasWord()) {
+            if (mWordsUntilSafeToSample > 0) {
+                mWordsUntilSafeToSample--;
+            }
+        }
+    }
+
+    public void resetWordCounter() {
+        mWordsUntilSafeToSample = mMinWordPeriod;
+    }
+
+    /**
+     * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete
+     * form and still protect the user's privacy.
+     *
+     * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any
+     * non-character data that is typed between words.  The decision about privacy is made based on
+     * the buffer's entire content.  If it is decided that the privacy risks are too great to upload
+     * the contents of this buffer, a censored version of the LogItems may still be uploaded.  E.g.,
+     * the screen orientation and other characteristics about the device can be uploaded without
+     * revealing much about the user.
+     */
+    public boolean isSafeToLog() {
+        // Check that we are not sampling too frequently.  Having sampled recently might disclose
+        // too much of the user's intended meaning.
+        if (mWordsUntilSafeToSample > 0) {
+            return false;
+        }
+        if (mSuggest == null || !mSuggest.hasMainDictionary()) {
+            // Main dictionary is unavailable.  Since we cannot check it, we cannot tell if a word
+            // is out-of-vocabulary or not.  Therefore, we must judge the entire buffer contents to
+            // potentially pose a privacy risk.
+            return false;
+        }
+        // Reload the dictionary in case it has changed (e.g., because the user has changed
+        // languages).
+        final Dictionary dictionary = mSuggest.getMainDictionary();
+        if (dictionary == null) {
+            return false;
+        }
+        // Check each word in the buffer.  If any word poses a privacy threat, we cannot upload the
+        // complete buffer contents in detail.
+        final int length = mLogUnits.size();
+        for (int i = 0; i < length; i++) {
+            final LogUnit logUnit = mLogUnits.get(i);
+            final String word = logUnit.getWord();
+            if (word == null) {
+                // Digits outside words are a privacy threat.
+                if (logUnit.hasDigit()) {
+                    return false;
+                }
+            } else {
+                // Words not in the dictionary are a privacy threat.
+                if (!(dictionary.isValidWord(word))) {
+                    return false;
+                }
+            }
+        }
+        // All checks have passed; this buffer's content can be safely uploaded.
+        return true;
+    }
+
+    @Override
+    protected void onShiftOut(LogUnit logUnit) {
+        if (mResearchLog != null) {
+            mResearchLog.publish(logUnit, false /* isIncludingPrivateData */);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index 18bf3c0..71a6d6a 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -26,7 +26,6 @@
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.research.ResearchLogger.LogUnit;
 
 import java.io.BufferedWriter;
 import java.io.File;
@@ -37,6 +36,7 @@
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -51,21 +51,22 @@
  */
 public class ResearchLog {
     private static final String TAG = ResearchLog.class.getSimpleName();
-    private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
-            new OutputStreamWriter(new NullOutputStream()));
+    private static final boolean DEBUG = false;
+    private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
+    private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4;
 
-    final ScheduledExecutorService mExecutor;
+    /* package */ final ScheduledExecutorService mExecutor;
     /* package */ final File mFile;
     private JsonWriter mJsonWriter = NULL_JSON_WRITER;
+    // true if at least one byte of data has been written out to the log file.  This must be
+    // remembered because JsonWriter requires that calls matching calls to beginObject and
+    // endObject, as well as beginArray and endArray, and the file is opened lazily, only when
+    // it is certain that data will be written.  Alternatively, the matching call exceptions
+    // could be caught, but this might suppress other errors.
+    private boolean mHasWrittenData = false;
 
-    private int mLoggingState;
-    private static final int LOGGING_STATE_UNSTARTED = 0;
-    private static final int LOGGING_STATE_READY = 1;   // don't create file until necessary
-    private static final int LOGGING_STATE_RUNNING = 2;
-    private static final int LOGGING_STATE_STOPPING = 3;
-    private static final int LOGGING_STATE_STOPPED = 4;
-    private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
-
+    private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
+            new OutputStreamWriter(new NullOutputStream()));
     private static class NullOutputStream extends OutputStream {
         /** {@inheritDoc} */
         @Override
@@ -84,128 +85,81 @@
         }
     }
 
-    public ResearchLog(File outputFile) {
-        mExecutor = Executors.newSingleThreadScheduledExecutor();
+    public ResearchLog(final File outputFile) {
         if (outputFile == null) {
             throw new IllegalArgumentException();
         }
+        mExecutor = Executors.newSingleThreadScheduledExecutor();
         mFile = outputFile;
-        mLoggingState = LOGGING_STATE_UNSTARTED;
     }
 
-    public synchronized void start() throws IOException {
-        switch (mLoggingState) {
-            case LOGGING_STATE_UNSTARTED:
-                mLoggingState = LOGGING_STATE_READY;
-                break;
-            case LOGGING_STATE_READY:
-            case LOGGING_STATE_RUNNING:
-            case LOGGING_STATE_STOPPING:
-            case LOGGING_STATE_STOPPED:
-                break;
-        }
-    }
-
-    public synchronized void stop() {
-        switch (mLoggingState) {
-            case LOGGING_STATE_UNSTARTED:
-                mLoggingState = LOGGING_STATE_STOPPED;
-                break;
-            case LOGGING_STATE_READY:
-            case LOGGING_STATE_RUNNING:
-                mExecutor.submit(new Callable<Object>() {
-                    @Override
-                    public Object call() throws Exception {
-                        try {
-                            mJsonWriter.endArray();
-                            mJsonWriter.flush();
-                            mJsonWriter.close();
-                        } finally {
-                            boolean success = mFile.setWritable(false, false);
-                            mLoggingState = LOGGING_STATE_STOPPED;
-                        }
-                        return null;
+    public synchronized void close() {
+        mExecutor.submit(new Callable<Object>() {
+            @Override
+            public Object call() throws Exception {
+                try {
+                    if (mHasWrittenData) {
+                        mJsonWriter.endArray();
+                        mJsonWriter.flush();
+                        mJsonWriter.close();
+                        mHasWrittenData = false;
                     }
-                });
-                removeAnyScheduledFlush();
-                mExecutor.shutdown();
-                mLoggingState = LOGGING_STATE_STOPPING;
-                break;
-            case LOGGING_STATE_STOPPING:
-            case LOGGING_STATE_STOPPED:
-        }
-    }
-
-    public boolean isAlive() {
-        switch (mLoggingState) {
-            case LOGGING_STATE_UNSTARTED:
-            case LOGGING_STATE_READY:
-            case LOGGING_STATE_RUNNING:
-                return true;
-        }
-        return false;
-    }
-
-    public void waitUntilStopped(final int timeoutInMs) throws InterruptedException {
+                } catch (Exception e) {
+                    Log.d(TAG, "error when closing ResearchLog:");
+                    e.printStackTrace();
+                } finally {
+                    if (mFile.exists()) {
+                        mFile.setWritable(false, false);
+                    }
+                }
+                return null;
+            }
+        });
         removeAnyScheduledFlush();
         mExecutor.shutdown();
-        mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS);
     }
 
+    private boolean mIsAbortSuccessful;
+
     public synchronized void abort() {
-        switch (mLoggingState) {
-            case LOGGING_STATE_UNSTARTED:
-                mLoggingState = LOGGING_STATE_STOPPED;
-                isAbortSuccessful = true;
-                break;
-            case LOGGING_STATE_READY:
-            case LOGGING_STATE_RUNNING:
-                mExecutor.submit(new Callable<Object>() {
-                    @Override
-                    public Object call() throws Exception {
-                        try {
-                            mJsonWriter.endArray();
-                            mJsonWriter.close();
-                        } finally {
-                            isAbortSuccessful = mFile.delete();
-                        }
-                        return null;
+        mExecutor.submit(new Callable<Object>() {
+            @Override
+            public Object call() throws Exception {
+                try {
+                    if (mHasWrittenData) {
+                        mJsonWriter.endArray();
+                        mJsonWriter.close();
+                        mHasWrittenData = false;
                     }
-                });
-                removeAnyScheduledFlush();
-                mExecutor.shutdown();
-                mLoggingState = LOGGING_STATE_STOPPING;
-                break;
-            case LOGGING_STATE_STOPPING:
-            case LOGGING_STATE_STOPPED:
-        }
+                } finally {
+                    mIsAbortSuccessful = mFile.delete();
+                }
+                return null;
+            }
+        });
+        removeAnyScheduledFlush();
+        mExecutor.shutdown();
     }
 
-    private boolean isAbortSuccessful;
-    public boolean isAbortSuccessful() {
-        return isAbortSuccessful;
+    public boolean blockingAbort() throws InterruptedException {
+        abort();
+        mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
+        return mIsAbortSuccessful;
+    }
+
+    public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException {
+        mExecutor.awaitTermination(delay, timeUnit);
     }
 
     /* package */ synchronized void flush() {
-        switch (mLoggingState) {
-            case LOGGING_STATE_UNSTARTED:
-                break;
-            case LOGGING_STATE_READY:
-            case LOGGING_STATE_RUNNING:
-                removeAnyScheduledFlush();
-                mExecutor.submit(mFlushCallable);
-                break;
-            case LOGGING_STATE_STOPPING:
-            case LOGGING_STATE_STOPPED:
-        }
+        removeAnyScheduledFlush();
+        mExecutor.submit(mFlushCallable);
     }
 
-    private Callable<Object> mFlushCallable = new Callable<Object>() {
+    private final Callable<Object> mFlushCallable = new Callable<Object>() {
         @Override
         public Object call() throws Exception {
-            if (mLoggingState == LOGGING_STATE_RUNNING) {
-                mJsonWriter.flush();
-            }
+            mJsonWriter.flush();
             return null;
         }
     };
@@ -224,56 +178,40 @@
         mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
     }
 
-    public synchronized void publishPublicEvents(final LogUnit logUnit) {
-        switch (mLoggingState) {
-            case LOGGING_STATE_UNSTARTED:
-                break;
-            case LOGGING_STATE_READY:
-            case LOGGING_STATE_RUNNING:
-                mExecutor.submit(new Callable<Object>() {
-                    @Override
-                    public Object call() throws Exception {
-                        logUnit.publishPublicEventsTo(ResearchLog.this);
-                        scheduleFlush();
-                        return null;
-                    }
-                });
-                break;
-            case LOGGING_STATE_STOPPING:
-            case LOGGING_STATE_STOPPED:
-        }
-    }
-
-    public synchronized void publishAllEvents(final LogUnit logUnit) {
-        switch (mLoggingState) {
-            case LOGGING_STATE_UNSTARTED:
-                break;
-            case LOGGING_STATE_READY:
-            case LOGGING_STATE_RUNNING:
-                mExecutor.submit(new Callable<Object>() {
-                    @Override
-                    public Object call() throws Exception {
-                        logUnit.publishAllEventsTo(ResearchLog.this);
-                        scheduleFlush();
-                        return null;
-                    }
-                });
-                break;
-            case LOGGING_STATE_STOPPING:
-            case LOGGING_STATE_STOPPED:
+    public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) {
+        try {
+            mExecutor.submit(new Callable<Object>() {
+                @Override
+                public Object call() throws Exception {
+                    logUnit.publishTo(ResearchLog.this, isIncludingPrivateData);
+                    scheduleFlush();
+                    return null;
+                }
+            });
+        } catch (RejectedExecutionException e) {
+            // TODO: Add code to record loss of data, and report.
         }
     }
 
     private static final String CURRENT_TIME_KEY = "_ct";
     private static final String UPTIME_KEY = "_ut";
     private static final String EVENT_TYPE_KEY = "_ty";
+
     void outputEvent(final String[] keys, final Object[] values) {
-        // not thread safe.
+        // Not thread safe.
+        if (keys.length == 0) {
+            return;
+        }
+        if (DEBUG) {
+            if (keys.length != values.length + 1) {
+                Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]);
+            }
+        }
         try {
             if (mJsonWriter == NULL_JSON_WRITER) {
                 mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
-                mJsonWriter.setLenient(true);
                 mJsonWriter.beginArray();
+                mHasWrittenData = true;
             }
             mJsonWriter.beginObject();
             mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
@@ -283,8 +221,8 @@
             for (int i = 0; i < length; i++) {
                 mJsonWriter.name(keys[i + 1]);
                 Object value = values[i];
-                if (value instanceof String) {
-                    mJsonWriter.value((String) value);
+                if (value instanceof CharSequence) {
+                    mJsonWriter.value(value.toString());
                 } else if (value instanceof Number) {
                     mJsonWriter.value((Number) value);
                 } else if (value instanceof Boolean) {
@@ -331,14 +269,11 @@
                     SuggestedWords words = (SuggestedWords) value;
                     mJsonWriter.beginObject();
                     mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
-                    mJsonWriter.name("willAutoCorrect")
-                        .value(words.mWillAutoCorrect);
+                    mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect);
                     mJsonWriter.name("isPunctuationSuggestions")
-                        .value(words.mIsPunctuationSuggestions);
-                    mJsonWriter.name("isObsoleteSuggestions")
-                        .value(words.mIsObsoleteSuggestions);
-                    mJsonWriter.name("isPrediction")
-                        .value(words.mIsPrediction);
+                            .value(words.mIsPunctuationSuggestions);
+                    mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions);
+                    mJsonWriter.name("isPrediction").value(words.mIsPrediction);
                     mJsonWriter.name("words");
                     mJsonWriter.beginArray();
                     final int size = words.size();
@@ -363,8 +298,8 @@
             try {
                 mJsonWriter.close();
             } catch (IllegalStateException e1) {
-                // assume that this is just the json not being terminated properly.
-                // ignore
+                // Assume that this is just the json not being terminated properly.
+                // Ignore
             } catch (IOException e1) {
                 e1.printStackTrace();
             } finally {
diff --git a/java/src/com/android/inputmethod/research/ResearchLogUploader.java b/java/src/com/android/inputmethod/research/ResearchLogUploader.java
deleted file mode 100644
index 3b12130..0000000
--- a/java/src/com/android/inputmethod/research/ResearchLogUploader.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * 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.research;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.BatteryManager;
-import android.util.Log;
-
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.R.string;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-public final class ResearchLogUploader {
-    private static final String TAG = ResearchLogUploader.class.getSimpleName();
-    private static final int UPLOAD_INTERVAL_IN_MS = 1000 * 60 * 15; // every 15 min
-    private static final int BUF_SIZE = 1024 * 8;
-
-    private final boolean mCanUpload;
-    private final Context mContext;
-    private final File mFilesDir;
-    private final URL mUrl;
-    private final ScheduledExecutorService mExecutor;
-
-    private Runnable doUploadRunnable = new UploadRunnable(null, false);
-
-    public ResearchLogUploader(final Context context, final File filesDir) {
-        mContext = context;
-        mFilesDir = filesDir;
-        final PackageManager packageManager = context.getPackageManager();
-        final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET,
-                context.getPackageName()) == PackageManager.PERMISSION_GRANTED;
-        if (!hasPermission) {
-            mCanUpload = false;
-            mUrl = null;
-            mExecutor = null;
-            return;
-        }
-        URL tempUrl = null;
-        boolean canUpload = false;
-        ScheduledExecutorService executor = null;
-        try {
-            final String urlString = context.getString(R.string.research_logger_upload_url);
-            if (urlString == null || urlString.equals("")) {
-                return;
-            }
-            tempUrl = new URL(urlString);
-            canUpload = true;
-            executor = Executors.newSingleThreadScheduledExecutor();
-        } catch (MalformedURLException e) {
-            tempUrl = null;
-            e.printStackTrace();
-            return;
-        } finally {
-            mCanUpload = canUpload;
-            mUrl = tempUrl;
-            mExecutor = executor;
-        }
-    }
-
-    public void start() {
-        if (mCanUpload) {
-            Log.d(TAG, "scheduling regular uploading");
-            mExecutor.scheduleWithFixedDelay(doUploadRunnable, UPLOAD_INTERVAL_IN_MS,
-                    UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS);
-        } else {
-            Log.d(TAG, "no permission to upload");
-        }
-    }
-
-    public void uploadNow(final Callback callback) {
-        // Perform an immediate upload.  Note that this should happen even if there is
-        // another upload happening right now, as it may have missed the latest changes.
-        // TODO: Reschedule regular upload tests starting from now.
-        if (mCanUpload) {
-            mExecutor.submit(new UploadRunnable(callback, true));
-        }
-    }
-
-    public interface Callback {
-        public void onUploadCompleted(final boolean success);
-    }
-
-    private boolean isExternallyPowered() {
-        final Intent intent = mContext.registerReceiver(null, new IntentFilter(
-                Intent.ACTION_BATTERY_CHANGED));
-        final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
-        return pluggedState == BatteryManager.BATTERY_PLUGGED_AC
-                || pluggedState == BatteryManager.BATTERY_PLUGGED_USB;
-    }
-
-    private boolean hasWifiConnection() {
-        final ConnectivityManager manager =
-                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-        final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
-        return wifiInfo.isConnected();
-    }
-
-    class UploadRunnable implements Runnable {
-        private final Callback mCallback;
-        private final boolean mForceUpload;
-
-        public UploadRunnable(final Callback callback, final boolean forceUpload) {
-            mCallback = callback;
-            mForceUpload = forceUpload;
-        }
-
-        @Override
-        public void run() {
-            doUpload();
-        }
-
-        private void doUpload() {
-            if (!mForceUpload && (!isExternallyPowered() || !hasWifiConnection())) {
-                return;
-            }
-            if (mFilesDir == null) {
-                return;
-            }
-            final File[] files = mFilesDir.listFiles(new FileFilter() {
-                @Override
-                public boolean accept(File pathname) {
-                    return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX)
-                            && !pathname.canWrite();
-                }
-            });
-            boolean success = true;
-            if (files.length == 0) {
-                success = false;
-            }
-            for (final File file : files) {
-                if (!uploadFile(file)) {
-                    success = false;
-                }
-            }
-            if (mCallback != null) {
-                mCallback.onUploadCompleted(success);
-            }
-        }
-
-        private boolean uploadFile(File file) {
-            Log.d(TAG, "attempting upload of " + file.getAbsolutePath());
-            boolean success = false;
-            final int contentLength = (int) file.length();
-            HttpURLConnection connection = null;
-            InputStream fileIs = null;
-            try {
-                fileIs = new FileInputStream(file);
-                connection = (HttpURLConnection) mUrl.openConnection();
-                connection.setRequestMethod("PUT");
-                connection.setDoOutput(true);
-                connection.setFixedLengthStreamingMode(contentLength);
-                final OutputStream os = connection.getOutputStream();
-                final byte[] buf = new byte[BUF_SIZE];
-                int numBytesRead;
-                while ((numBytesRead = fileIs.read(buf)) != -1) {
-                    os.write(buf, 0, numBytesRead);
-                }
-                if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
-                    Log.d(TAG, "upload failed: " + connection.getResponseCode());
-                    InputStream netIs = connection.getInputStream();
-                    BufferedReader reader = new BufferedReader(new InputStreamReader(netIs));
-                    String line;
-                    while ((line = reader.readLine()) != null) {
-                        Log.d(TAG, "| " + reader.readLine());
-                    }
-                    reader.close();
-                    return success;
-                }
-                file.delete();
-                success = true;
-                Log.d(TAG, "upload successful");
-            } catch (Exception e) {
-                e.printStackTrace();
-            } finally {
-                if (fileIs != null) {
-                    try {
-                        fileIs.close();
-                    } catch (IOException e) {
-                        e.printStackTrace();
-                    }
-                }
-                if (connection != null) {
-                    connection.disconnect();
-                }
-            }
-            return success;
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index cf6f31a..918fcf5 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -18,11 +18,14 @@
 
 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
 
+import android.app.AlarmManager;
 import android.app.AlertDialog;
 import android.app.Dialog;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 import android.content.pm.PackageInfo;
@@ -34,15 +37,18 @@
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.SystemClock;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.Log;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.widget.Button;
@@ -51,9 +57,10 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.KeyboardView;
 import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.R;
@@ -64,11 +71,8 @@
 import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.File;
-import java.io.IOException;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
 import java.util.Date;
-import java.util.List;
 import java.util.Locale;
 import java.util.UUID;
 
@@ -94,24 +98,26 @@
             new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
     private static final boolean IS_SHOWING_INDICATOR = true;
     private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false;
+    public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
 
     // 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";
-    private static final int ABORT_TIMEOUT_IN_MS = 10 * 1000; // timeout to notify user
 
     private static final ResearchLogger sInstance = new ResearchLogger();
     // to write to a different filename, e.g., for testing, set mFile before calling start()
     /* package */ File mFilesDir;
     /* package */ String mUUIDString;
     /* package */ ResearchLog mMainResearchLog;
-    // The mIntentionalResearchLog records all events for the session, private or not (excepting
+    // mFeedbackLog records all events for the session, private or not (excepting
     // passwords).  It is written to permanent storage only if the user explicitly commands
     // the system to do so.
-    /* package */ ResearchLog mIntentionalResearchLog;
-    // LogUnits are queued here and released only when the user requests the intentional log.
-    private List<LogUnit> mIntentionalResearchLogQueue = new ArrayList<LogUnit>();
+    // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
+    // complete.
+    /* package */ ResearchLog mFeedbackLog;
+    /* package */ MainLogBuffer mMainLogBuffer;
+    /* package */ LogBuffer mFeedbackLogBuffer;
 
     private boolean mIsPasswordView = false;
     private boolean mIsLoggingSuspended = false;
@@ -133,20 +139,24 @@
     // used to check whether words are not unique
     private Suggest mSuggest;
     private Dictionary mDictionary;
-    private KeyboardSwitcher mKeyboardSwitcher;
+    private MainKeyboardView mMainKeyboardView;
     private InputMethodService mInputMethodService;
+    private final Statistics mStatistics;
 
-    private ResearchLogUploader mResearchLogUploader;
+    private Intent mUploadIntent;
+    private PendingIntent mUploadPendingIntent;
+
+    private LogUnit mCurrentLogUnit = new LogUnit();
 
     private ResearchLogger() {
+        mStatistics = Statistics.getInstance();
     }
 
     public static ResearchLogger getInstance() {
         return sInstance;
     }
 
-    public void init(final InputMethodService ims, final SharedPreferences prefs,
-            KeyboardSwitcher keyboardSwitcher) {
+    public void init(final InputMethodService ims, final SharedPreferences prefs) {
         assert ims != null;
         if (ims == null) {
             Log.w(TAG, "IMS is null; logging is off");
@@ -176,11 +186,33 @@
                 e.apply();
             }
         }
-        mResearchLogUploader = new ResearchLogUploader(ims, mFilesDir);
-        mResearchLogUploader.start();
-        mKeyboardSwitcher = keyboardSwitcher;
         mInputMethodService = ims;
         mPrefs = prefs;
+        mUploadIntent = new Intent(mInputMethodService, UploaderService.class);
+        mUploadPendingIntent = PendingIntent.getService(mInputMethodService, 0, mUploadIntent, 0);
+
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            scheduleUploadingService(mInputMethodService);
+        }
+    }
+
+    /**
+     * Arrange for the UploaderService to be run on a regular basis.
+     *
+     * Any existing scheduled invocation of UploaderService is removed and rescheduled.  This may
+     * cause problems if this method is called often and frequent updates are required, but since
+     * the user will likely be sleeping at some point, if the interval is less that the expected
+     * sleep duration and this method is not called during that time, the service should be invoked
+     * at some point.
+     */
+    public static void scheduleUploadingService(Context context) {
+        final Intent intent = new Intent(context, UploaderService.class);
+        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+        final AlarmManager manager =
+                (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        manager.cancel(pendingIntent);
+        manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
     }
 
     private void cleanupLoggingDir(final File dir, final long time) {
@@ -192,10 +224,15 @@
         }
     }
 
-    public void mainKeyboardView_onAttachedToWindow() {
+    public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
+        mMainKeyboardView = mainKeyboardView;
         maybeShowSplashScreen();
     }
 
+    public void mainKeyboardView_onDetachedFromWindow() {
+        mMainKeyboardView = null;
+    }
+
     private boolean hasSeenSplash() {
         return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false);
     }
@@ -209,7 +246,8 @@
         if (mSplashDialog != null && mSplashDialog.isShowing()) {
             return;
         }
-        final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
+        final IBinder windowToken = mMainKeyboardView != null
+                ? mMainKeyboardView.getWindowToken() : null;
         if (windowToken == null) {
             return;
         }
@@ -257,50 +295,7 @@
         final Editor e = mPrefs.edit();
         e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true);
         e.apply();
-    }
-
-    private File createLogFile(File filesDir) {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(FILENAME_PREFIX).append('-');
-        sb.append(mUUIDString).append('-');
-        sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
-        sb.append(FILENAME_SUFFIX);
-        return new File(filesDir, sb.toString());
-    }
-
-    private void start() {
-        maybeShowSplashScreen();
-        updateSuspendedState();
-        requestIndicatorRedraw();
-        if (!isAllowedToLog()) {
-            // Log.w(TAG, "not in usability mode; not logging");
-            return;
-        }
-        if (mFilesDir == null || !mFilesDir.exists()) {
-            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
-            return;
-        }
-        try {
-            if (mMainResearchLog == null || !mMainResearchLog.isAlive()) {
-                mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
-            }
-            mMainResearchLog.start();
-            if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) {
-                mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir));
-            }
-            mIntentionalResearchLog.start();
-        } catch (IOException e) {
-            Log.w(TAG, "Could not start ResearchLogger.");
-        }
-    }
-
-    /* package */ void stop() {
-        if (mMainResearchLog != null) {
-            mMainResearchLog.stop();
-        }
-        if (mIntentionalResearchLog != null) {
-            mIntentionalResearchLog.stop();
-        }
+        restart();
     }
 
     private void setLoggingAllowed(boolean enableLogging) {
@@ -313,40 +308,104 @@
         sIsLogging = enableLogging;
     }
 
-    public boolean abort() {
-        boolean didAbortMainLog = false;
-        if (mMainResearchLog != null) {
-            mMainResearchLog.abort();
-            try {
-                mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS);
-            } catch (InterruptedException e) {
-                // interrupted early.  carry on.
-            }
-            if (mMainResearchLog.isAbortSuccessful()) {
-                didAbortMainLog = true;
-            }
-            mMainResearchLog = null;
-        }
-        boolean didAbortIntentionalLog = false;
-        if (mIntentionalResearchLog != null) {
-            mIntentionalResearchLog.abort();
-            try {
-                mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS);
-            } catch (InterruptedException e) {
-                // interrupted early.  carry on.
-            }
-            if (mIntentionalResearchLog.isAbortSuccessful()) {
-                didAbortIntentionalLog = true;
-            }
-            mIntentionalResearchLog = null;
-        }
-        return didAbortMainLog && didAbortIntentionalLog;
+    private File createLogFile(File filesDir) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(FILENAME_PREFIX).append('-');
+        sb.append(mUUIDString).append('-');
+        sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
+        sb.append(FILENAME_SUFFIX);
+        return new File(filesDir, sb.toString());
     }
 
-    /* package */ void flush() {
-        if (mMainResearchLog != null) {
-            mMainResearchLog.flush();
+    private void checkForEmptyEditor() {
+        if (mInputMethodService == null) {
+            return;
         }
+        final InputConnection ic = mInputMethodService.getCurrentInputConnection();
+        if (ic == null) {
+            return;
+        }
+        final CharSequence textBefore = ic.getTextBeforeCursor(1, 0);
+        if (!TextUtils.isEmpty(textBefore)) {
+            mStatistics.setIsEmptyUponStarting(false);
+            return;
+        }
+        final CharSequence textAfter = ic.getTextAfterCursor(1, 0);
+        if (!TextUtils.isEmpty(textAfter)) {
+            mStatistics.setIsEmptyUponStarting(false);
+            return;
+        }
+        if (textBefore != null && textAfter != null) {
+            mStatistics.setIsEmptyUponStarting(true);
+        }
+    }
+
+    private void start() {
+        maybeShowSplashScreen();
+        updateSuspendedState();
+        requestIndicatorRedraw();
+        mStatistics.reset();
+        checkForEmptyEditor();
+        if (!isAllowedToLog()) {
+            // Log.w(TAG, "not in usability mode; not logging");
+            return;
+        }
+        if (mFilesDir == null || !mFilesDir.exists()) {
+            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
+            return;
+        }
+        if (mMainLogBuffer == null) {
+            mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
+            mMainLogBuffer = new MainLogBuffer(mMainResearchLog);
+            mMainLogBuffer.setSuggest(mSuggest);
+        }
+        if (mFeedbackLogBuffer == null) {
+            mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
+            // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
+            // the feedback LogUnit itself.
+            mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
+        }
+    }
+
+    /* package */ void stop() {
+        logStatistics();
+        commitCurrentLogUnit();
+
+        if (mMainLogBuffer != null) {
+            publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */);
+            mMainResearchLog.close();
+            mMainLogBuffer = null;
+        }
+        if (mFeedbackLogBuffer != null) {
+            mFeedbackLog.close();
+            mFeedbackLogBuffer = null;
+        }
+    }
+
+    public boolean abort() {
+        boolean didAbortMainLog = false;
+        if (mMainLogBuffer != null) {
+            mMainLogBuffer.clear();
+            try {
+                didAbortMainLog = mMainResearchLog.blockingAbort();
+            } catch (InterruptedException e) {
+                // Don't know whether this succeeded or not.  We assume not; this is reported
+                // to the caller.
+            }
+            mMainLogBuffer = null;
+        }
+        boolean didAbortFeedbackLog = false;
+        if (mFeedbackLogBuffer != null) {
+            mFeedbackLogBuffer.clear();
+            try {
+                didAbortFeedbackLog = mFeedbackLog.blockingAbort();
+            } catch (InterruptedException e) {
+                // Don't know whether this succeeded or not.  We assume not; this is reported
+                // to the caller.
+            }
+            mFeedbackLogBuffer = null;
+        }
+        return didAbortMainLog && didAbortFeedbackLog;
     }
 
     private void restart() {
@@ -387,6 +446,8 @@
             abort();
         }
         requestIndicatorRedraw();
+        mPrefs = prefs;
+        prefsChanged(prefs);
     }
 
     public void presentResearchDialog(final LatinIME latinIME) {
@@ -450,79 +511,44 @@
         latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
     }
 
-    private ResearchLog mFeedbackLog;
-    private List<LogUnit> mFeedbackQueue;
-    private ResearchLog mSavedMainResearchLog;
-    private ResearchLog mSavedIntentionalResearchLog;
-    private List<LogUnit> mSavedIntentionalResearchLogQueue;
-
-    private void saveLogsForFeedback() {
-        mFeedbackLog = mIntentionalResearchLog;
-        if (mIntentionalResearchLogQueue != null) {
-            mFeedbackQueue = new ArrayList<LogUnit>(mIntentionalResearchLogQueue);
-        } else {
-            mFeedbackQueue = null;
+    private static final String[] EVENTKEYS_FEEDBACK = {
+        "UserTimestamp", "contents"
+    };
+    public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
+        if (mFeedbackLogBuffer == null) {
+            return;
         }
-        mSavedMainResearchLog = mMainResearchLog;
-        mSavedIntentionalResearchLog = mIntentionalResearchLog;
-        mSavedIntentionalResearchLogQueue = mIntentionalResearchLogQueue;
-
-        mMainResearchLog = null;
-        mIntentionalResearchLog = null;
-        mIntentionalResearchLogQueue = new ArrayList<LogUnit>();
+        if (includeHistory) {
+            commitCurrentLogUnit();
+        } else {
+            mFeedbackLogBuffer.clear();
+        }
+        final LogUnit feedbackLogUnit = new LogUnit();
+        final Object[] values = {
+            feedbackContents
+        };
+        feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values,
+                false /* isPotentiallyPrivate */);
+        mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
+        publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
+        mFeedbackLog.close();
+        uploadNow();
+        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
     }
 
-    private static final int LOG_DRAIN_TIMEOUT_IN_MS = 1000 * 5;
-    public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
-        if (includeHistory && mFeedbackLog != null) {
-            try {
-                LogUnit headerLogUnit = new LogUnit();
-                headerLogUnit.addLogAtom(EVENTKEYS_INTENTIONAL_LOG, EVENTKEYS_NULLVALUES, false);
-                mFeedbackLog.publishAllEvents(headerLogUnit);
-                for (LogUnit logUnit : mFeedbackQueue) {
-                    mFeedbackLog.publishAllEvents(logUnit);
-                }
-                userFeedback(mFeedbackLog, feedbackContents);
-                mFeedbackLog.stop();
-                try {
-                    mFeedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir));
-                mIntentionalResearchLog.start();
-            } catch (IOException e) {
-                e.printStackTrace();
-            } finally {
-                mIntentionalResearchLogQueue.clear();
-            }
-            mResearchLogUploader.uploadNow(null);
-        } else {
-            // create a separate ResearchLog just for feedback
-            final ResearchLog feedbackLog = new ResearchLog(createLogFile(mFilesDir));
-            try {
-                feedbackLog.start();
-                userFeedback(feedbackLog, feedbackContents);
-                feedbackLog.stop();
-                feedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS);
-                mResearchLogUploader.uploadNow(null);
-            } catch (IOException e) {
-                e.printStackTrace();
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-        }
+    public void uploadNow() {
+        mInputMethodService.startService(mUploadIntent);
     }
 
     public void onLeavingSendFeedbackDialog() {
         mInFeedbackDialog = false;
-        mMainResearchLog = mSavedMainResearchLog;
-        mIntentionalResearchLog = mSavedIntentionalResearchLog;
-        mIntentionalResearchLogQueue = mSavedIntentionalResearchLogQueue;
     }
 
     public void initSuggest(Suggest suggest) {
         mSuggest = suggest;
+        if (mMainLogBuffer != null) {
+            mMainLogBuffer.setSuggest(mSuggest);
+        }
     }
 
     private void setIsPasswordView(boolean isPasswordView) {
@@ -530,21 +556,17 @@
     }
 
     private boolean isAllowedToLog() {
-        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging;
+        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
     }
 
     public void requestIndicatorRedraw() {
         if (!IS_SHOWING_INDICATOR) {
             return;
         }
-        if (mKeyboardSwitcher == null) {
+        if (mMainKeyboardView == null) {
             return;
         }
-        final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
-        if (mainKeyboardView == null) {
-            return;
-        }
-        mainKeyboardView.invalidateAllKeys();
+        mMainKeyboardView.invalidateAllKeys();
     }
 
 
@@ -577,13 +599,8 @@
         }
     }
 
-    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.
      *
@@ -599,10 +616,14 @@
             final Object[] values) {
         assert values.length + 1 == keys.length;
         if (isAllowedToLog()) {
-            mCurrentLogUnit.addLogAtom(keys, values, true);
+            mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */);
         }
     }
 
+    private void setCurrentLogUnitContainsDigitFlag() {
+        mCurrentLogUnit.setContainsDigit();
+    }
+
     /**
      * Buffer a research log event, flaggint it as not privacy-sensitive.
      *
@@ -618,139 +639,54 @@
     private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
         assert values.length + 1 == keys.length;
         if (isAllowedToLog()) {
-            mCurrentLogUnit.addLogAtom(keys, values, false);
+            mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */);
         }
     }
 
-    // Used to track how often words are logged.  Too-frequent logging can leak
-    // semantics, disclosing private data.
-    /* package for test */ static class LoggingFrequencyState {
-        private static final int DEFAULT_WORD_LOG_FREQUENCY = 10;
-        private int mWordsRemainingToSkip;
-        private final int mFrequency;
-
-        /**
-         * Tracks how often words may be uploaded.
-         *
-         * @param frequency 1=Every word, 2=Every other word, etc.
-         */
-        public LoggingFrequencyState(int frequency) {
-            mFrequency = frequency;
-            mWordsRemainingToSkip = mFrequency;
-        }
-
-        public void onWordLogged() {
-            mWordsRemainingToSkip = mFrequency;
-        }
-
-        public void onWordNotLogged() {
-            if (mWordsRemainingToSkip > 1) {
-                mWordsRemainingToSkip--;
-            }
-        }
-
-        public boolean isSafeToLog() {
-            return mWordsRemainingToSkip <= 1;
-        }
-    }
-
-    /* package for test */ LoggingFrequencyState mLoggingFrequencyState =
-            new LoggingFrequencyState(LoggingFrequencyState.DEFAULT_WORD_LOG_FREQUENCY);
-
-    /* package for test */ boolean isPrivacyThreat(String word) {
-        // Current checks:
-        // - Word not in dictionary
-        // - Word contains numbers
-        // - Privacy-safe word not logged recently
-        if (TextUtils.isEmpty(word)) {
-            return false;
-        }
-        if (!mLoggingFrequencyState.isSafeToLog()) {
-            return true;
-        }
-        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;
-    }
-
-    private void onWordComplete(String word) {
-        if (isPrivacyThreat(word)) {
-            publishLogUnit(mCurrentLogUnit, true);
-            mLoggingFrequencyState.onWordNotLogged();
-        } else {
-            publishLogUnit(mCurrentLogUnit, false);
-            mLoggingFrequencyState.onWordLogged();
-        }
-        mCurrentLogUnit = new LogUnit();
-    }
-
-    private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) {
-        if (!isAllowedToLog()) {
-            return;
-        }
-        if (mMainResearchLog == null) {
-            return;
-        }
-        if (isPrivacySensitive) {
-            mMainResearchLog.publishPublicEvents(logUnit);
-        } else {
-            mMainResearchLog.publishAllEvents(logUnit);
-        }
-        mIntentionalResearchLogQueue.add(logUnit);
-    }
-
-    /* package */ void publishCurrentLogUnit(ResearchLog researchLog, boolean isPrivacySensitive) {
-        publishLogUnit(mCurrentLogUnit, isPrivacySensitive);
-    }
-
-    static class LogUnit {
-        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 void addLogAtom(final String[] keys, final Object[] values,
-                final Boolean isPotentiallyPrivate) {
-            mKeysList.add(keys);
-            mValuesList.add(values);
-            mIsPotentiallyPrivate.add(isPotentiallyPrivate);
-        }
-
-        public void publishPublicEventsTo(ResearchLog researchLog) {
-            final int size = mKeysList.size();
-            for (int i = 0; i < size; i++) {
-                if (!mIsPotentiallyPrivate.get(i)) {
-                    researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i));
+    /* package for test */ void commitCurrentLogUnit() {
+        if (!mCurrentLogUnit.isEmpty()) {
+            if (mMainLogBuffer != null) {
+                mMainLogBuffer.shiftIn(mCurrentLogUnit);
+                if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) {
+                    publishLogBuffer(mMainLogBuffer, mMainResearchLog,
+                            true /* isIncludingPrivateData */);
+                    mMainLogBuffer.resetWordCounter();
                 }
             }
+            if (mFeedbackLogBuffer != null) {
+                mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
+            }
+            mCurrentLogUnit = new LogUnit();
+            Log.d(TAG, "commitCurrentLogUnit");
         }
+    }
 
-        public void publishAllEventsTo(ResearchLog researchLog) {
-            final int size = mKeysList.size();
-            for (int i = 0; i < size; i++) {
-                researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i));
+    /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
+            final ResearchLog researchLog, final boolean isIncludingPrivateData) {
+        LogUnit logUnit;
+        while ((logUnit = logBuffer.shiftOut()) != null) {
+            researchLog.publish(logUnit, isIncludingPrivateData);
+        }
+    }
+
+    private boolean hasOnlyLetters(final String word) {
+        final int length = word.length();
+        for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+            final int codePoint = word.codePointAt(i);
+            if (!Character.isLetter(codePoint)) {
+                return false;
             }
         }
+        return true;
+    }
+
+    private void onWordComplete(final String word) {
+        Log.d(TAG, "onWordComplete: " + word);
+        if (word != null && word.length() > 0 && hasOnlyLetters(word)) {
+            mCurrentLogUnit.setWord(word);
+            mStatistics.recordWordEntered();
+        }
+        commitCurrentLogUnit();
     }
 
     private static int scrubDigitFromCodePoint(int codePoint) {
@@ -803,12 +739,6 @@
         return WORD_REPLACEMENT_STRING;
     }
 
-    // Special methods related to startup, shutdown, logging itself
-
-    private static final String[] EVENTKEYS_INTENTIONAL_LOG = {
-        "IntentionalLog"
-    };
-
     private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = {
         "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
         "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion"
@@ -816,9 +746,6 @@
     public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
             final SharedPreferences prefs) {
         final ResearchLogger researchLogger = getInstance();
-        if (researchLogger.mInFeedbackDialog) {
-            researchLogger.saveLogsForFeedback();
-        }
         researchLogger.start();
         if (editorInfo != null) {
             final Context context = researchLogger.mInputMethodService;
@@ -846,32 +773,19 @@
         stop();
     }
 
-    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.onWordComplete(scrubbedWord);
-    }
-
     private static final String[] EVENTKEYS_USER_FEEDBACK = {
         "UserFeedback", "FeedbackContents"
     };
 
-    private void userFeedback(ResearchLog researchLog, String feedbackContents) {
-        // this method is special; it directs the feedbackContents to a particular researchLog
-        final LogUnit logUnit = new LogUnit();
+    private static final String[] EVENTKEYS_PREFS_CHANGED = {
+        "PrefsChanged", "prefs"
+    };
+    public static void prefsChanged(final SharedPreferences prefs) {
+        final ResearchLogger researchLogger = getInstance();
         final Object[] values = {
-            feedbackContents
+            prefs
         };
-        logUnit.addLogAtom(EVENTKEYS_USER_FEEDBACK, values, false);
-        researchLog.publishAllEvents(logUnit);
+        researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values);
     }
 
     // Regular logging methods
@@ -908,51 +822,16 @@
         "LatinIMEOnCodeInput", "code", "x", "y"
     };
     public static void latinIME_onCodeInput(final int code, final int x, final int y) {
+        final long time = SystemClock.uptimeMillis();
+        final ResearchLogger researchLogger = getInstance();
         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);
-    }
-
-    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);
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+        if (Character.isDigit(code)) {
+            researchLogger.setCurrentLogUnitContainsDigitFlag();
+        }
+        researchLogger.mStatistics.recordChar(code, time);
     }
 
     private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
@@ -979,6 +858,10 @@
     public static void latinIME_onWindowHidden(final int savedSelectionStart,
             final int savedSelectionEnd, final InputConnection ic) {
         if (ic != null) {
+            // Capture the TextView contents.  This will trigger onUpdateSelection(), so we
+            // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
+            // it can tell that it was generated by the logging code, and not by the user, and
+            // therefore keep user-visible state as is.
             ic.beginBatchEdit();
             ic.performContextMenuAction(android.R.id.selectAll);
             CharSequence charSequence = ic.getSelectedText(0);
@@ -1013,9 +896,7 @@
             }
             final ResearchLogger researchLogger = getInstance();
             researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
-            // Play it safe.  Remove privacy-sensitive events.
-            researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true);
-            researchLogger.mCurrentLogUnit = new LogUnit();
+            researchLogger.commitCurrentLogUnit();
             getInstance().stop();
         }
     }
@@ -1048,37 +929,15 @@
         researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
     }
 
-    private static final String[] EVENTKEYS_LATINIME_PERFORMEDITORACTION = {
-        "LatinIMEPerformEditorAction", "imeActionNext"
-    };
-    public static void latinIME_performEditorAction(final int 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 cs, int x, int y) {
-        final Object[] values = {
-            index, cs, x, y
-        };
-        final ResearchLogger researchLogger = getInstance();
-        researchLogger.enqueuePotentiallyPrivateEvent(
-                EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values);
-    }
-
     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) {
+            final int index, CharSequence suggestion) {
         final Object[] values = {
-            scrubDigitsFromString(replacedWord), index, suggestion == null ? null :
-                    scrubDigitsFromString(suggestion.toString()), x, y
+            scrubDigitsFromString(replacedWord), index,
+            (suggestion == null ? null : scrubDigitsFromString(suggestion.toString())),
+            Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE
         };
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY,
@@ -1089,28 +948,14 @@
         "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y"
     };
     public static void latinIME_punctuationSuggestion(final int index,
-            final CharSequence suggestion, int x, int y) {
+            final CharSequence suggestion) {
         final Object[] values = {
-            index, suggestion, x, y
+            index, suggestion,
+            Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE
         };
         getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
     }
 
-    private static final String[] EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT = {
-        "LatinIMERevertDoubleSpaceWhileInBatchEdit"
-    };
-    public static void latinIME_revertDoubleSpaceWhileInBatchEdit() {
-        getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT,
-                EVENTKEYS_NULLVALUES);
-    }
-
-    private static final String[] EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION = {
-        "LatinIMERevertSwapPunctuation"
-    };
-    public static void latinIME_revertSwapPunctuation() {
-        getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES);
-    }
-
     private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = {
         "LatinIMESendKeyCodePoint", "code"
     };
@@ -1118,15 +963,18 @@
         final Object[] values = {
             Keyboard.printableCode(scrubDigitFromCodePoint(code))
         };
-        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
+        if (Character.isDigit(code)) {
+            researchLogger.setCurrentLogUnitContainsDigitFlag();
+        }
     }
 
-    private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = {
-        "LatinIMESwapSwapperAndSpaceWhileInBatchEdit"
+    private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = {
+        "LatinIMESwapSwapperAndSpace"
     };
-    public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() {
-        getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT,
-                EVENTKEYS_NULLVALUES);
+    public static void latinIME_swapSwapperAndSpace() {
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES);
     }
 
     private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = {
@@ -1245,6 +1093,128 @@
         getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values);
     }
 
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = {
+        "RichInputConnectionCommitCompletion", "completionInfo"
+    };
+    public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
+        final Object[] values = {
+            completionInfo
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(
+                EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values);
+    }
+
+    // Disabled for privacy-protection reasons.  Because this event comes after
+    // richInputConnection_commitText, which is the event used to separate LogUnits, the
+    // data in this event can be associated with the next LogUnit, revealing information
+    // about the current word even if it was supposed to be suppressed.  The occurrance of
+    // autocorrection can be determined by examining the difference between the text strings in
+    // the last call to richInputConnection_setComposingText before
+    // richInputConnection_commitText, so it's not a data loss.
+    // TODO: Figure out how to log this event without loss of privacy.
+    /*
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = {
+        "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection"
+    };
+    */
+    public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) {
+        /*
+        final String typedWord = correctionInfo.getOldText().toString();
+        final String autoCorrection = correctionInfo.getNewText().toString();
+        final Object[] values = {
+            scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection)
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(
+                EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values);
+        */
+    }
+
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = {
+        "RichInputConnectionCommitText", "typedWord", "newCursorPosition"
+    };
+    public static void richInputConnection_commitText(final CharSequence typedWord,
+            final int newCursorPosition) {
+        final String scrubbedWord = scrubDigitsFromString(typedWord.toString());
+        final Object[] values = {
+            scrubbedWord, newCursorPosition
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT,
+                values);
+        researchLogger.onWordComplete(scrubbedWord);
+    }
+
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = {
+        "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength"
+    };
+    public static void richInputConnection_deleteSurroundingText(final int beforeLength,
+            final int afterLength) {
+        final Object[] values = {
+            beforeLength, afterLength
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(
+                EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values);
+    }
+
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = {
+        "RichInputConnectionFinishComposingText"
+    };
+    public static void richInputConnection_finishComposingText() {
+        getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT,
+                EVENTKEYS_NULLVALUES);
+    }
+
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = {
+        "RichInputConnectionPerformEditorAction", "imeActionNext"
+    };
+    public static void richInputConnection_performEditorAction(final int imeActionNext) {
+        final Object[] values = {
+            imeActionNext
+        };
+        getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values);
+    }
+
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = {
+        "RichInputConnectionSendKeyEvent", "eventTime", "action", "code"
+    };
+    public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
+        final Object[] values = {
+            keyEvent.getEventTime(),
+            keyEvent.getAction(),
+            keyEvent.getKeyCode()
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT,
+                values);
+    }
+
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = {
+        "RichInputConnectionSetComposingText", "text", "newCursorPosition"
+    };
+    public static void richInputConnection_setComposingText(final CharSequence text,
+            final int newCursorPosition) {
+        if (text == null) {
+            throw new RuntimeException("setComposingText is null");
+        }
+        final Object[] values = {
+            text, newCursorPosition
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT,
+                values);
+    }
+
+    private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = {
+        "RichInputConnectionSetSelection", "from", "to"
+    };
+    public static void richInputConnection_setSelection(final int from, final int to) {
+        final Object[] values = {
+            from, to
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION,
+                values);
+    }
+
     private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
         "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent"
     };
@@ -1277,4 +1247,24 @@
     public void userTimestamp() {
         getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
     }
+
+    private static final String[] EVENTKEYS_STATISTICS = {
+        "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount",
+        "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys",
+        "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete"
+    };
+    private static void logStatistics() {
+        final ResearchLogger researchLogger = getInstance();
+        final Statistics statistics = researchLogger.mStatistics;
+        final Object[] values = {
+            statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount,
+            statistics.mSpaceCount, statistics.mDeleteKeyCount,
+            statistics.mWordCount, statistics.mIsEmptyUponStarting,
+            statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
+            statistics.mBeforeDeleteKeyCounter.getAverageTime(),
+            statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
+            statistics.mAfterDeleteKeyCounter.getAverageTime()
+        };
+        researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values);
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
new file mode 100644
index 0000000..eab465a
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -0,0 +1,146 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.keyboard.Keyboard;
+
+public class Statistics {
+    // Number of characters entered during a typing session
+    int mCharCount;
+    // Number of letter characters entered during a typing session
+    int mLetterCount;
+    // Number of number characters entered
+    int mNumberCount;
+    // Number of space characters entered
+    int mSpaceCount;
+    // Number of delete operations entered (taps on the backspace key)
+    int mDeleteKeyCount;
+    // Number of words entered during a session.
+    int mWordCount;
+    // Whether the text field was empty upon editing
+    boolean mIsEmptyUponStarting;
+    boolean mIsEmptinessStateKnown;
+
+    // Timers to count average time to enter a key, first press a delete key,
+    // between delete keys, and then to return typing after a delete key.
+    final AverageTimeCounter mKeyCounter = new AverageTimeCounter();
+    final AverageTimeCounter mBeforeDeleteKeyCounter = new AverageTimeCounter();
+    final AverageTimeCounter mDuringRepeatedDeleteKeysCounter = new AverageTimeCounter();
+    final AverageTimeCounter mAfterDeleteKeyCounter = new AverageTimeCounter();
+
+    static class AverageTimeCounter {
+        int mCount;
+        int mTotalTime;
+
+        public void reset() {
+            mCount = 0;
+            mTotalTime = 0;
+        }
+
+        public void add(long deltaTime) {
+            mCount++;
+            mTotalTime += deltaTime;
+        }
+
+        public int getAverageTime() {
+            if (mCount == 0) {
+                return 0;
+            }
+            return mTotalTime / mCount;
+        }
+    }
+
+    // To account for the interruptions when the user's attention is directed elsewhere, times
+    // longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic.
+    public static final int MIN_TYPING_INTERMISSION = 2 * 1000;  // in milliseconds
+    public static final int MIN_DELETION_INTERMISSION = 10 * 1000;  // in milliseconds
+
+    // The last time that a tap was performed
+    private long mLastTapTime;
+    // The type of the last keypress (delete key or not)
+    boolean mIsLastKeyDeleteKey;
+
+    private static final Statistics sInstance = new Statistics();
+
+    public static Statistics getInstance() {
+        return sInstance;
+    }
+
+    private Statistics() {
+        reset();
+    }
+
+    public void reset() {
+        mCharCount = 0;
+        mLetterCount = 0;
+        mNumberCount = 0;
+        mSpaceCount = 0;
+        mDeleteKeyCount = 0;
+        mWordCount = 0;
+        mIsEmptyUponStarting = true;
+        mIsEmptinessStateKnown = false;
+        mKeyCounter.reset();
+        mBeforeDeleteKeyCounter.reset();
+        mDuringRepeatedDeleteKeysCounter.reset();
+        mAfterDeleteKeyCounter.reset();
+
+        mLastTapTime = 0;
+        mIsLastKeyDeleteKey = false;
+    }
+
+    public void recordChar(int codePoint, long time) {
+        final long delta = time - mLastTapTime;
+        if (codePoint == Keyboard.CODE_DELETE) {
+            mDeleteKeyCount++;
+            if (delta < MIN_DELETION_INTERMISSION) {
+                if (mIsLastKeyDeleteKey) {
+                    mDuringRepeatedDeleteKeysCounter.add(delta);
+                } else {
+                    mBeforeDeleteKeyCounter.add(delta);
+                }
+            }
+            mIsLastKeyDeleteKey = true;
+        } else {
+            mCharCount++;
+            if (Character.isDigit(codePoint)) {
+                mNumberCount++;
+            }
+            if (Character.isLetter(codePoint)) {
+                mLetterCount++;
+            }
+            if (Character.isSpaceChar(codePoint)) {
+                mSpaceCount++;
+            }
+            if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) {
+                mAfterDeleteKeyCounter.add(delta);
+            } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) {
+                mKeyCounter.add(delta);
+            }
+            mIsLastKeyDeleteKey = false;
+        }
+        mLastTapTime = time;
+    }
+
+    public void recordWordEntered() {
+        mWordCount++;
+    }
+
+    public void setIsEmptyUponStarting(final boolean isEmpty) {
+        mIsEmptyUponStarting = isEmpty;
+        mIsEmptinessStateKnown = true;
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
new file mode 100644
index 0000000..7a57490
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -0,0 +1,191 @@
+/*
+ * 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.research;
+
+import android.Manifest;
+import android.app.AlarmManager;
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.inputmethod.latin.R;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public final class UploaderService extends IntentService {
+    private static final String TAG = UploaderService.class.getSimpleName();
+    public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
+    private static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+            + ".extra.UPLOAD_UNCONDITIONALLY";
+    private static final int BUF_SIZE = 1024 * 8;
+    protected static final int TIMEOUT_IN_MS = 1000 * 4;
+
+    private boolean mCanUpload;
+    private File mFilesDir;
+    private URL mUrl;
+
+    public UploaderService() {
+        super("Research Uploader Service");
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        mCanUpload = false;
+        mFilesDir = null;
+        mUrl = null;
+
+        final PackageManager packageManager = getPackageManager();
+        final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET,
+                getPackageName()) == PackageManager.PERMISSION_GRANTED;
+        if (!hasPermission) {
+            return;
+        }
+
+        try {
+            final String urlString = getString(R.string.research_logger_upload_url);
+            if (urlString == null || urlString.equals("")) {
+                return;
+            }
+            mFilesDir = getFilesDir();
+            mUrl = new URL(urlString);
+            mCanUpload = true;
+        } catch (MalformedURLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        if (!mCanUpload) {
+            return;
+        }
+        boolean isUploadingUnconditionally = false;
+        Bundle bundle = intent.getExtras();
+        if (bundle != null && bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) {
+            isUploadingUnconditionally = bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY);
+        }
+        doUpload(isUploadingUnconditionally);
+    }
+
+    private boolean isExternallyPowered() {
+        final Intent intent = registerReceiver(null, new IntentFilter(
+                Intent.ACTION_BATTERY_CHANGED));
+        final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+        return pluggedState == BatteryManager.BATTERY_PLUGGED_AC
+                || pluggedState == BatteryManager.BATTERY_PLUGGED_USB;
+    }
+
+    private boolean hasWifiConnection() {
+        final ConnectivityManager manager =
+                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+        final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+        return wifiInfo.isConnected();
+    }
+
+    private void doUpload(final boolean isUploadingUnconditionally) {
+        if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection())) {
+            return;
+        }
+        if (mFilesDir == null) {
+            return;
+        }
+        final File[] files = mFilesDir.listFiles(new FileFilter() {
+            @Override
+            public boolean accept(File pathname) {
+                return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX)
+                        && !pathname.canWrite();
+            }
+        });
+        boolean success = true;
+        if (files.length == 0) {
+            success = false;
+        }
+        for (final File file : files) {
+            if (!uploadFile(file)) {
+                success = false;
+            }
+        }
+    }
+
+    private boolean uploadFile(File file) {
+        Log.d(TAG, "attempting upload of " + file.getAbsolutePath());
+        boolean success = false;
+        final int contentLength = (int) file.length();
+        HttpURLConnection connection = null;
+        InputStream fileInputStream = null;
+        try {
+            fileInputStream = new FileInputStream(file);
+            connection = (HttpURLConnection) mUrl.openConnection();
+            connection.setRequestMethod("PUT");
+            connection.setDoOutput(true);
+            connection.setFixedLengthStreamingMode(contentLength);
+            final OutputStream os = connection.getOutputStream();
+            final byte[] buf = new byte[BUF_SIZE];
+            int numBytesRead;
+            while ((numBytesRead = fileInputStream.read(buf)) != -1) {
+                os.write(buf, 0, numBytesRead);
+            }
+            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                Log.d(TAG, "upload failed: " + connection.getResponseCode());
+                InputStream netInputStream = connection.getInputStream();
+                BufferedReader reader = new BufferedReader(new InputStreamReader(netInputStream));
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    Log.d(TAG, "| " + reader.readLine());
+                }
+                reader.close();
+                return success;
+            }
+            file.delete();
+            success = true;
+            Log.d(TAG, "upload successful");
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (fileInputStream != null) {
+                try {
+                    fileInputStream.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+        return success;
+    }
+}
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index 2fd8982..567648f 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -36,7 +36,7 @@
 LATIN_IME_JNI_SRC_FILES := \
     com_android_inputmethod_keyboard_ProximityInfo.cpp \
     com_android_inputmethod_latin_BinaryDictionary.cpp \
-    com_android_inputmethod_latin_NativeUtils.cpp \
+    com_android_inputmethod_latin_DicTraverseSession.cpp \
     jni_common.cpp
 
 LATIN_IME_CORE_SRC_FILES := \
@@ -46,6 +46,7 @@
     char_utils.cpp \
     correction.cpp \
     dictionary.cpp \
+    dic_traverse_wrapper.cpp \
     proximity_info.cpp \
     proximity_info_state.cpp \
     unigram_dictionary.cpp \
diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
index 74390cc..560b3a5 100644
--- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
+++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
@@ -16,8 +16,6 @@
 
 #define LOG_TAG "LatinIME: jni: ProximityInfo"
 
-#include <string>
-
 #include "com_android_inputmethod_keyboard_ProximityInfo.h"
 #include "jni.h"
 #include "jni_common.h"
@@ -26,53 +24,27 @@
 namespace latinime {
 
 static jlong latinime_Keyboard_setProximityInfo(JNIEnv *env, jobject object,
-        jstring localejStr, jint maxProximityCharsSize, jint displayWidth, jint displayHeight,
-        jint gridWidth, jint gridHeight, jint mostCommonkeyWidth, jintArray proximityCharsArray,
-        jint keyCount, jintArray keyXCoordinateArray, jintArray keyYCoordinateArray,
-        jintArray keyWidthArray, jintArray keyHeightArray, jintArray keyCharCodeArray,
-        jfloatArray sweetSpotCenterXArray, jfloatArray sweetSpotCenterYArray,
-        jfloatArray sweetSpotRadiusArray) {
-    const char *localeStrPtr = env->GetStringUTFChars(localejStr, 0);
-    const std::string localeStr(localeStrPtr);
-    jint *proximityChars = env->GetIntArrayElements(proximityCharsArray, 0);
-    jint *keyXCoordinates = safeGetIntArrayElements(env, keyXCoordinateArray);
-    jint *keyYCoordinates = safeGetIntArrayElements(env, keyYCoordinateArray);
-    jint *keyWidths = safeGetIntArrayElements(env, keyWidthArray);
-    jint *keyHeights = safeGetIntArrayElements(env, keyHeightArray);
-    jint *keyCharCodes = safeGetIntArrayElements(env, keyCharCodeArray);
-    jfloat *sweetSpotCenterXs = safeGetFloatArrayElements(env, sweetSpotCenterXArray);
-    jfloat *sweetSpotCenterYs = safeGetFloatArrayElements(env, sweetSpotCenterYArray);
-    jfloat *sweetSpotRadii = safeGetFloatArrayElements(env, sweetSpotRadiusArray);
-    ProximityInfo *proximityInfo = new ProximityInfo(
-            localeStr, maxProximityCharsSize, displayWidth, displayHeight, gridWidth, gridHeight,
-            mostCommonkeyWidth, (const int32_t*)proximityChars, keyCount,
-            (const int32_t*)keyXCoordinates, (const int32_t*)keyYCoordinates,
-            (const int32_t*)keyWidths, (const int32_t*)keyHeights, (const int32_t*)keyCharCodes,
-            (const float*)sweetSpotCenterXs, (const float*)sweetSpotCenterYs,
-            (const float*)sweetSpotRadii);
-    safeReleaseFloatArrayElements(env, sweetSpotRadiusArray, sweetSpotRadii);
-    safeReleaseFloatArrayElements(env, sweetSpotCenterYArray, sweetSpotCenterYs);
-    safeReleaseFloatArrayElements(env, sweetSpotCenterXArray, sweetSpotCenterXs);
-    safeReleaseIntArrayElements(env, keyCharCodeArray, keyCharCodes);
-    safeReleaseIntArrayElements(env, keyHeightArray, keyHeights);
-    safeReleaseIntArrayElements(env, keyWidthArray, keyWidths);
-    safeReleaseIntArrayElements(env, keyYCoordinateArray, keyYCoordinates);
-    safeReleaseIntArrayElements(env, keyXCoordinateArray, keyXCoordinates);
-    env->ReleaseIntArrayElements(proximityCharsArray, proximityChars, 0);
-    env->ReleaseStringUTFChars(localejStr, localeStrPtr);
-    return (jlong)proximityInfo;
+        jstring localeJStr, jint maxProximityCharsSize, jint displayWidth, jint displayHeight,
+        jint gridWidth, jint gridHeight, jint mostCommonkeyWidth, jintArray proximityChars,
+        jint keyCount, jintArray keyXCoordinates, jintArray keyYCoordinates,
+        jintArray keyWidths, jintArray keyHeights, jintArray keyCharCodes,
+        jfloatArray sweetSpotCenterXs, jfloatArray sweetSpotCenterYs, jfloatArray sweetSpotRadii) {
+    ProximityInfo *proximityInfo = new ProximityInfo(env, localeJStr, maxProximityCharsSize,
+            displayWidth, displayHeight, gridWidth, gridHeight, mostCommonkeyWidth, proximityChars,
+            keyCount, keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
+            sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
+    return reinterpret_cast<jlong>(proximityInfo);
 }
 
 static void latinime_Keyboard_release(JNIEnv *env, jobject object, jlong proximityInfo) {
-    ProximityInfo *pi = (ProximityInfo*)proximityInfo;
-    if (!pi) return;
+    ProximityInfo *pi = reinterpret_cast<ProximityInfo *>(proximityInfo);
     delete pi;
 }
 
 static JNINativeMethod sKeyboardMethods[] = {
     {"setProximityInfoNative", "(Ljava/lang/String;IIIIII[II[I[I[I[I[I[F[F[F)J",
-            (void*)latinime_Keyboard_setProximityInfo},
-    {"releaseProximityInfoNative", "(J)V", (void*)latinime_Keyboard_release}
+            reinterpret_cast<void *>(latinime_Keyboard_setProximityInfo)},
+    {"releaseProximityInfoNative", "(J)V", reinterpret_cast<void *>(latinime_Keyboard_release)}
 };
 
 int register_ProximityInfo(JNIEnv *env) {
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 776f5f7..a20958a 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -14,15 +14,12 @@
  * limitations under the License.
  */
 
+
+#include <cstring> // for memset()
+
 #define LOG_TAG "LatinIME: jni: BinaryDictionary"
 
-#include "binary_format.h"
-#include "com_android_inputmethod_latin_BinaryDictionary.h"
-#include "correction.h"
-#include "defines.h"
-#include "dictionary.h"
-#include "jni.h"
-#include "jni_common.h"
+#include "defines.h" // for macros below
 
 #ifdef USE_MMAP_FOR_DICTIONARY
 #include <cerrno>
@@ -30,13 +27,21 @@
 #include <sys/mman.h>
 #else // USE_MMAP_FOR_DICTIONARY
 #include <cstdlib>
+#include <cstdio> // for fopen() etc.
 #endif // USE_MMAP_FOR_DICTIONARY
 
+#include "binary_format.h"
+#include "com_android_inputmethod_latin_BinaryDictionary.h"
+#include "correction.h"
+#include "dictionary.h"
+#include "jni.h"
+#include "jni_common.h"
+
 namespace latinime {
 
 class ProximityInfo;
 
-void releaseDictBuf(void *dictBuf, const size_t length, int fd);
+static void releaseDictBuf(const void *dictBuf, const size_t length, const int fd);
 
 static jlong latinime_BinaryDictionary_open(JNIEnv *env, jobject object,
         jstring sourceDir, jlong dictOffset, jlong dictSize,
@@ -44,11 +49,14 @@
         jint maxPredictions) {
     PROF_OPEN;
     PROF_START(66);
-    const char *sourceDirChars = env->GetStringUTFChars(sourceDir, 0);
-    if (sourceDirChars == 0) {
+    const jsize sourceDirUtf8Length = env->GetStringUTFLength(sourceDir);
+    if (sourceDirUtf8Length <= 0) {
         AKLOGE("DICT: Can't get sourceDir string");
         return 0;
     }
+    char sourceDirChars[sourceDirUtf8Length + 1];
+    env->GetStringUTFRegion(sourceDir, 0, env->GetStringLength(sourceDir), sourceDirChars);
+    sourceDirChars[sourceDirUtf8Length] = '\0';
     int fd = 0;
     void *dictBuf = 0;
     int adjust = 0;
@@ -68,7 +76,7 @@
         AKLOGE("DICT: Can't mmap dictionary. errno=%d", errno);
         return 0;
     }
-    dictBuf = (void *)((char *)dictBuf + adjust);
+    dictBuf = static_cast<char *>(dictBuf) + adjust;
 #else // USE_MMAP_FOR_DICTIONARY
     /* malloc version */
     FILE *file = 0;
@@ -98,17 +106,16 @@
         return 0;
     }
 #endif // USE_MMAP_FOR_DICTIONARY
-    env->ReleaseStringUTFChars(sourceDir, sourceDirChars);
-
     if (!dictBuf) {
         AKLOGE("DICT: dictBuf is null");
         return 0;
     }
     Dictionary *dictionary = 0;
-    if (BinaryFormat::UNKNOWN_FORMAT == BinaryFormat::detectFormat((uint8_t*)dictBuf)) {
+    if (BinaryFormat::UNKNOWN_FORMAT
+            == BinaryFormat::detectFormat(static_cast<uint8_t *>(dictBuf))) {
         AKLOGE("DICT: dictionary format is unknown, bad magic number");
 #ifdef USE_MMAP_FOR_DICTIONARY
-        releaseDictBuf(((char*)dictBuf) - adjust, adjDictSize, fd);
+        releaseDictBuf(static_cast<const char *>(dictBuf) - adjust, adjDictSize, fd);
 #else // USE_MMAP_FOR_DICTIONARY
         releaseDictBuf(dictBuf, 0, 0);
 #endif // USE_MMAP_FOR_DICTIONARY
@@ -122,106 +129,131 @@
 }
 
 static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jobject object, jlong dict,
-        jlong proximityInfo, jintArray xCoordinatesArray, jintArray yCoordinatesArray,
-        jintArray timesArray, jintArray pointerIdArray, jintArray inputArray, jint arraySize,
-        jint commitPoint, jboolean isGesture,
-        jintArray prevWordForBigrams, jboolean useFullEditDistance, jcharArray outputArray,
-        jintArray frequencyArray, jintArray spaceIndexArray, jintArray outputTypesArray) {
-    Dictionary *dictionary = (Dictionary*) dict;
+        jlong proximityInfo, jlong dicTraverseSession, jintArray xCoordinatesArray,
+        jintArray yCoordinatesArray, jintArray timesArray, jintArray pointerIdsArray,
+        jintArray inputCodePointsArray, jint arraySize, jint commitPoint, jboolean isGesture,
+        jintArray prevWordCodePointsForBigrams, jboolean useFullEditDistance,
+        jcharArray outputCharsArray, jintArray scoresArray, jintArray spaceIndicesArray,
+        jintArray outputTypesArray) {
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return 0;
-    ProximityInfo *pInfo = (ProximityInfo*)proximityInfo;
-    int *xCoordinates = env->GetIntArrayElements(xCoordinatesArray, 0);
-    int *yCoordinates = env->GetIntArrayElements(yCoordinatesArray, 0);
-    int *times = env->GetIntArrayElements(timesArray, 0);
-    int *pointerIds = env->GetIntArrayElements(pointerIdArray, 0);
-    int *frequencies = env->GetIntArrayElements(frequencyArray, 0);
-    int *inputCodes = env->GetIntArrayElements(inputArray, 0);
-    jchar *outputChars = env->GetCharArrayElements(outputArray, 0);
-    int *spaceIndices = env->GetIntArrayElements(spaceIndexArray, 0);
-    int *outputTypes = env->GetIntArrayElements(outputTypesArray, 0);
-    jint *prevWordChars = prevWordForBigrams
-            ? env->GetIntArrayElements(prevWordForBigrams, 0) : 0;
-    jsize prevWordLength = prevWordChars ? env->GetArrayLength(prevWordForBigrams) : 0;
+    ProximityInfo *pInfo = reinterpret_cast<ProximityInfo *>(proximityInfo);
+    void *traverseSession = reinterpret_cast<void *>(dicTraverseSession);
+
+    // Input values
+    int xCoordinates[arraySize];
+    int yCoordinates[arraySize];
+    int times[arraySize];
+    int pointerIds[arraySize];
+    const jsize inputCodePointsLength = env->GetArrayLength(inputCodePointsArray);
+    int inputCodePoints[inputCodePointsLength];
+    const jsize prevWordCodePointsLength =
+            prevWordCodePointsForBigrams ? env->GetArrayLength(prevWordCodePointsForBigrams) : 0;
+    int prevWordCodePointsInternal[prevWordCodePointsLength];
+    int *prevWordCodePoints = 0;
+    env->GetIntArrayRegion(xCoordinatesArray, 0, arraySize, xCoordinates);
+    env->GetIntArrayRegion(yCoordinatesArray, 0, arraySize, yCoordinates);
+    env->GetIntArrayRegion(timesArray, 0, arraySize, times);
+    env->GetIntArrayRegion(pointerIdsArray, 0, arraySize, pointerIds);
+    env->GetIntArrayRegion(inputCodePointsArray, 0, inputCodePointsLength, inputCodePoints);
+    if (prevWordCodePointsForBigrams) {
+        env->GetIntArrayRegion(prevWordCodePointsForBigrams, 0, prevWordCodePointsLength,
+                prevWordCodePointsInternal);
+        prevWordCodePoints = prevWordCodePointsInternal;
+    }
+
+    // Output values
+    // TODO: Should be "outputCodePointsLength" and "int outputCodePoints[]"
+    const jsize outputCharsLength = env->GetArrayLength(outputCharsArray);
+    unsigned short outputChars[outputCharsLength];
+    const jsize scoresLength = env->GetArrayLength(scoresArray);
+    int scores[scoresLength];
+    const jsize spaceIndicesLength = env->GetArrayLength(spaceIndicesArray);
+    int spaceIndices[spaceIndicesLength];
+    const jsize outputTypesLength = env->GetArrayLength(outputTypesArray);
+    int outputTypes[outputTypesLength];
+    memset(outputChars, 0, sizeof(outputChars));
+    memset(scores, 0, sizeof(scores));
+    memset(spaceIndices, 0, sizeof(spaceIndices));
+    memset(outputTypes, 0, sizeof(outputTypes));
 
     int count;
-    if (isGesture || arraySize > 1) {
-        count = dictionary->getSuggestions(pInfo, xCoordinates, yCoordinates, times, pointerIds,
-                inputCodes, arraySize, prevWordChars, prevWordLength, commitPoint, isGesture,
-                useFullEditDistance, (unsigned short*) outputChars, frequencies, spaceIndices,
-                outputTypes);
+    if (isGesture || arraySize > 0) {
+        count = dictionary->getSuggestions(pInfo, traverseSession, xCoordinates, yCoordinates,
+                times, pointerIds, inputCodePoints, arraySize, prevWordCodePoints,
+                prevWordCodePointsLength, commitPoint, isGesture, useFullEditDistance, outputChars,
+                scores, spaceIndices, outputTypes);
     } else {
-        count = dictionary->getBigrams(prevWordChars, prevWordLength, inputCodes,
-                arraySize, (unsigned short*) outputChars, frequencies, outputTypes);
+        count = dictionary->getBigrams(prevWordCodePoints, prevWordCodePointsLength,
+                inputCodePoints, arraySize, outputChars, scores, outputTypes);
     }
 
-    if (prevWordChars) {
-        env->ReleaseIntArrayElements(prevWordForBigrams, prevWordChars, JNI_ABORT);
-    }
-    env->ReleaseIntArrayElements(outputTypesArray, outputTypes, 0);
-    env->ReleaseIntArrayElements(spaceIndexArray, spaceIndices, 0);
-    env->ReleaseCharArrayElements(outputArray, outputChars, 0);
-    env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT);
-    env->ReleaseIntArrayElements(frequencyArray, frequencies, 0);
-    env->ReleaseIntArrayElements(pointerIdArray, pointerIds, 0);
-    env->ReleaseIntArrayElements(timesArray, times, 0);
-    env->ReleaseIntArrayElements(yCoordinatesArray, yCoordinates, 0);
-    env->ReleaseIntArrayElements(xCoordinatesArray, xCoordinates, 0);
+    // Copy back the output values
+    // TODO: Should be SetIntArrayRegion()
+    env->SetCharArrayRegion(outputCharsArray, 0, outputCharsLength, outputChars);
+    env->SetIntArrayRegion(scoresArray, 0, scoresLength, scores);
+    env->SetIntArrayRegion(spaceIndicesArray, 0, spaceIndicesLength, spaceIndices);
+    env->SetIntArrayRegion(outputTypesArray, 0, outputTypesLength, outputTypes);
+
     return count;
 }
 
 static jint latinime_BinaryDictionary_getFrequency(JNIEnv *env, jobject object, jlong dict,
-        jintArray wordArray, jint wordLength) {
-    Dictionary *dictionary = (Dictionary*)dict;
-    if (!dictionary) return (jboolean) false;
-    jint *word = env->GetIntArrayElements(wordArray, 0);
-    jint result = dictionary->getFrequency(word, wordLength);
-    env->ReleaseIntArrayElements(wordArray, word, JNI_ABORT);
-    return result;
+        jintArray wordArray) {
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
+    if (!dictionary) return 0;
+    const jsize codePointLength = env->GetArrayLength(wordArray);
+    int codePoints[codePointLength];
+    env->GetIntArrayRegion(wordArray, 0, codePointLength, codePoints);
+    return dictionary->getFrequency(codePoints, codePointLength);
 }
 
 static jboolean latinime_BinaryDictionary_isValidBigram(JNIEnv *env, jobject object, jlong dict,
         jintArray wordArray1, jintArray wordArray2) {
-    Dictionary *dictionary = (Dictionary*)dict;
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return (jboolean) false;
-    jint *word1 = env->GetIntArrayElements(wordArray1, 0);
-    jint *word2 = env->GetIntArrayElements(wordArray2, 0);
-    jsize length1 = word1 ? env->GetArrayLength(wordArray1) : 0;
-    jsize length2 = word2 ? env->GetArrayLength(wordArray2) : 0;
-    jboolean result = dictionary->isValidBigram(word1, length1, word2, length2);
-    env->ReleaseIntArrayElements(wordArray2, word2, JNI_ABORT);
-    env->ReleaseIntArrayElements(wordArray1, word1, JNI_ABORT);
-    return result;
+    const jsize codePointLength1 = env->GetArrayLength(wordArray1);
+    const jsize codePointLength2 = env->GetArrayLength(wordArray2);
+    int codePoints1[codePointLength1];
+    int codePoints2[codePointLength2];
+    env->GetIntArrayRegion(wordArray1, 0, codePointLength1, codePoints1);
+    env->GetIntArrayRegion(wordArray2, 0, codePointLength2, codePoints2);
+    return dictionary->isValidBigram(codePoints1, codePointLength1, codePoints2, codePointLength2);
 }
 
 static jfloat latinime_BinaryDictionary_calcNormalizedScore(JNIEnv *env, jobject object,
-        jcharArray before, jint beforeLength, jcharArray after, jint afterLength, jint score) {
-    jchar *beforeChars = env->GetCharArrayElements(before, 0);
-    jchar *afterChars = env->GetCharArrayElements(after, 0);
-    jfloat result = Correction::RankingAlgorithm::calcNormalizedScore((unsigned short*)beforeChars,
-            beforeLength, (unsigned short*)afterChars, afterLength, score);
-    env->ReleaseCharArrayElements(after, afterChars, JNI_ABORT);
-    env->ReleaseCharArrayElements(before, beforeChars, JNI_ABORT);
-    return result;
+        jcharArray before, jcharArray after, jint score) {
+    jsize beforeLength = env->GetArrayLength(before);
+    jsize afterLength = env->GetArrayLength(after);
+    jchar beforeChars[beforeLength];
+    jchar afterChars[afterLength];
+    env->GetCharArrayRegion(before, 0, beforeLength, beforeChars);
+    env->GetCharArrayRegion(after, 0, afterLength, afterChars);
+    return Correction::RankingAlgorithm::calcNormalizedScore(
+            static_cast<unsigned short *>(beforeChars), beforeLength,
+            static_cast<unsigned short *>(afterChars), afterLength, score);
 }
 
 static jint latinime_BinaryDictionary_editDistance(JNIEnv *env, jobject object,
-        jcharArray before, jint beforeLength, jcharArray after, jint afterLength) {
-    jchar *beforeChars = env->GetCharArrayElements(before, 0);
-    jchar *afterChars = env->GetCharArrayElements(after, 0);
-    jint result = Correction::RankingAlgorithm::editDistance(
-            (unsigned short*)beforeChars, beforeLength, (unsigned short*)afterChars, afterLength);
-    env->ReleaseCharArrayElements(after, afterChars, JNI_ABORT);
-    env->ReleaseCharArrayElements(before, beforeChars, JNI_ABORT);
-    return result;
+        jcharArray before, jcharArray after) {
+    jsize beforeLength = env->GetArrayLength(before);
+    jsize afterLength = env->GetArrayLength(after);
+    jchar beforeChars[beforeLength];
+    jchar afterChars[afterLength];
+    env->GetCharArrayRegion(before, 0, beforeLength, beforeChars);
+    env->GetCharArrayRegion(after, 0, afterLength, afterChars);
+    return Correction::RankingAlgorithm::editDistance(
+            static_cast<unsigned short *>(beforeChars), beforeLength,
+            static_cast<unsigned short *>(afterChars), afterLength);
 }
 
 static void latinime_BinaryDictionary_close(JNIEnv *env, jobject object, jlong dict) {
-    Dictionary *dictionary = (Dictionary*)dict;
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return;
-    void *dictBuf = dictionary->getDict();
+    const void *dictBuf = dictionary->getDict();
     if (!dictBuf) return;
 #ifdef USE_MMAP_FOR_DICTIONARY
-    releaseDictBuf((void *)((char *)dictBuf - dictionary->getDictBufAdjust()),
+    releaseDictBuf(static_cast<const char *>(dictBuf) - dictionary->getDictBufAdjust(),
             dictionary->getDictSize() + dictionary->getDictBufAdjust(), dictionary->getMmapFd());
 #else // USE_MMAP_FOR_DICTIONARY
     releaseDictBuf(dictBuf, 0, 0);
@@ -229,9 +261,9 @@
     delete dictionary;
 }
 
-void releaseDictBuf(void *dictBuf, const size_t length, int fd) {
+static void releaseDictBuf(const void *dictBuf, const size_t length, const int fd) {
 #ifdef USE_MMAP_FOR_DICTIONARY
-    int ret = munmap(dictBuf, length);
+    int ret = munmap(const_cast<void *>(dictBuf), length);
     if (ret != 0) {
         AKLOGE("DICT: Failure in munmap. ret=%d errno=%d", ret, errno);
     }
@@ -240,20 +272,24 @@
         AKLOGE("DICT: Failure in close. ret=%d errno=%d", ret, errno);
     }
 #else // USE_MMAP_FOR_DICTIONARY
-    free(dictBuf);
+    free(const_cast<void *>(dictBuf));
 #endif // USE_MMAP_FOR_DICTIONARY
 }
 
 static JNINativeMethod sMethods[] = {
-    {"openNative", "(Ljava/lang/String;JJIIIII)J", (void*)latinime_BinaryDictionary_open},
-    {"closeNative", "(J)V", (void*)latinime_BinaryDictionary_close},
-    {"getSuggestionsNative", "(JJ[I[I[I[I[IIIZ[IZ[C[I[I[I)I",
-            (void*) latinime_BinaryDictionary_getSuggestions},
-    {"getFrequencyNative", "(J[II)I", (void*)latinime_BinaryDictionary_getFrequency},
-    {"isValidBigramNative", "(J[I[I)Z", (void*)latinime_BinaryDictionary_isValidBigram},
-    {"calcNormalizedScoreNative", "([CI[CII)F",
-            (void*)latinime_BinaryDictionary_calcNormalizedScore},
-    {"editDistanceNative", "([CI[CI)I", (void*)latinime_BinaryDictionary_editDistance}
+    {"openNative", "(Ljava/lang/String;JJIIIII)J",
+            reinterpret_cast<void *>(latinime_BinaryDictionary_open)},
+    {"closeNative", "(J)V", reinterpret_cast<void *>(latinime_BinaryDictionary_close)},
+    {"getSuggestionsNative", "(JJJ[I[I[I[I[IIIZ[IZ[C[I[I[I)I",
+            reinterpret_cast<void *>(latinime_BinaryDictionary_getSuggestions)},
+    {"getFrequencyNative", "(J[I)I",
+            reinterpret_cast<void *>(latinime_BinaryDictionary_getFrequency)},
+    {"isValidBigramNative", "(J[I[I)Z",
+            reinterpret_cast<void *>(latinime_BinaryDictionary_isValidBigram)},
+    {"calcNormalizedScoreNative", "([C[CI)F",
+            reinterpret_cast<void *>(latinime_BinaryDictionary_calcNormalizedScore)},
+    {"editDistanceNative", "([C[C)I",
+            reinterpret_cast<void *>(latinime_BinaryDictionary_editDistance)}
 };
 
 int register_BinaryDictionary(JNIEnv *env) {
diff --git a/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp b/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
new file mode 100644
index 0000000..5d405f1
--- /dev/null
+++ b/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "LatinIME: jni: Session"
+
+#include "com_android_inputmethod_latin_DicTraverseSession.h"
+#include "dic_traverse_wrapper.h"
+#include "jni.h"
+#include "jni_common.h"
+
+namespace latinime {
+class Dictionary;
+static jlong latinime_setDicTraverseSession(JNIEnv *env, jobject object, jstring localeJStr) {
+    void *traverseSession = DicTraverseWrapper::getDicTraverseSession(env, localeJStr);
+    return reinterpret_cast<jlong>(traverseSession);
+}
+
+static void latinime_initDicTraverseSession(JNIEnv *env, jobject object, jlong traverseSession,
+        jlong dictionary, jintArray previousWord, jint previousWordLength) {
+    void *ts = reinterpret_cast<void *>(traverseSession);
+    Dictionary *dict = reinterpret_cast<Dictionary *>(dictionary);
+    if (!previousWord) {
+        DicTraverseWrapper::initDicTraverseSession(ts, dict, 0, 0);
+        return;
+    }
+    int prevWord[previousWordLength];
+    env->GetIntArrayRegion(previousWord, 0, previousWordLength, prevWord);
+    DicTraverseWrapper::initDicTraverseSession(ts, dict, prevWord, previousWordLength);
+}
+
+static void latinime_releaseDicTraverseSession(JNIEnv *env, jobject object, jlong traverseSession) {
+    void *ts = reinterpret_cast<void *>(traverseSession);
+    DicTraverseWrapper::releaseDicTraverseSession(ts);
+}
+
+static JNINativeMethod sMethods[] = {
+    {"setDicTraverseSessionNative", "(Ljava/lang/String;)J",
+            reinterpret_cast<void *>(latinime_setDicTraverseSession)},
+    {"initDicTraverseSessionNative", "(JJ[II)V",
+            reinterpret_cast<void *>(latinime_initDicTraverseSession)},
+    {"releaseDicTraverseSessionNative", "(J)V",
+            reinterpret_cast<void *>(latinime_releaseDicTraverseSession)}
+};
+
+int register_DicTraverseSession(JNIEnv *env) {
+    const char *const kClassPathName = "com/android/inputmethod/latin/DicTraverseSession";
+    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_DicTraverseSession.h
similarity index 73%
rename from native/jni/com_android_inputmethod_latin_NativeUtils.h
rename to native/jni/com_android_inputmethod_latin_DicTraverseSession.h
index d1ffb8f..37531e9 100644
--- a/native/jni/com_android_inputmethod_latin_NativeUtils.h
+++ b/native/jni/com_android_inputmethod_latin_DicTraverseSession.h
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-#ifndef _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
-#define _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
+#ifndef _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
+#define _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
 
+#include "defines.h"
 #include "jni.h"
 
 namespace latinime {
-
-int register_NativeUtils(JNIEnv *env);
-
+int register_DicTraverseSession(JNIEnv *env);
 } // namespace latinime
-#endif // _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
+#endif // _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
diff --git a/native/jni/com_android_inputmethod_latin_NativeUtils.cpp b/native/jni/com_android_inputmethod_latin_NativeUtils.cpp
deleted file mode 100644
index 8f1afbe..0000000
--- a/native/jni/com_android_inputmethod_latin_NativeUtils.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 "com_android_inputmethod_latin_NativeUtils.h"
-#include "jni.h"
-#include "jni_common.h"
-
-#include <cmath>
-
-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/jni_common.cpp b/native/jni/jni_common.cpp
index 105a4dc..0da1669 100644
--- a/native/jni/jni_common.cpp
+++ b/native/jni/jni_common.cpp
@@ -16,15 +16,15 @@
 
 #define LOG_TAG "LatinIME: jni"
 
+#include <cassert>
+
 #include "com_android_inputmethod_keyboard_ProximityInfo.h"
 #include "com_android_inputmethod_latin_BinaryDictionary.h"
-#include "com_android_inputmethod_latin_NativeUtils.h"
+#include "com_android_inputmethod_latin_DicTraverseSession.h"
 #include "defines.h"
 #include "jni.h"
 #include "jni_common.h"
 
-#include <cassert>
-
 using namespace latinime;
 
 /*
@@ -34,7 +34,7 @@
     JNIEnv *env = 0;
     jint result = -1;
 
-    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
         AKLOGE("ERROR: GetEnv failed");
         goto bail;
     }
@@ -45,13 +45,13 @@
         goto bail;
     }
 
-    if (!register_ProximityInfo(env)) {
-        AKLOGE("ERROR: ProximityInfo native registration failed");
+    if (!register_DicTraverseSession(env)) {
+        AKLOGE("ERROR: DicTraverseSession native registration failed");
         goto bail;
     }
 
-    if (!register_NativeUtils(env)) {
-        AKLOGE("ERROR: NativeUtils native registration failed");
+    if (!register_ProximityInfo(env)) {
+        AKLOGE("ERROR: ProximityInfo native registration failed");
         goto bail;
     }
 
diff --git a/native/jni/jni_common.h b/native/jni/jni_common.h
index 658ff18..993f97e 100644
--- a/native/jni/jni_common.h
+++ b/native/jni/jni_common.h
@@ -24,32 +24,5 @@
 int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods,
         int numMethods);
 
-inline jint *safeGetIntArrayElements(JNIEnv *env, jintArray jArray) {
-    if (jArray) {
-        return env->GetIntArrayElements(jArray, 0);
-    } else {
-        return 0;
-    }
-}
-
-inline jfloat *safeGetFloatArrayElements(JNIEnv *env, jfloatArray jArray) {
-    if (jArray) {
-        return env->GetFloatArrayElements(jArray, 0);
-    } else {
-        return 0;
-    }
-}
-
-inline void safeReleaseIntArrayElements(JNIEnv *env, jintArray jArray, jint *cArray) {
-    if (jArray) {
-        env->ReleaseIntArrayElements(jArray, cArray, 0);
-    }
-}
-
-inline void safeReleaseFloatArrayElements(JNIEnv *env, jfloatArray jArray, jfloat *cArray) {
-    if (jArray) {
-        env->ReleaseFloatArrayElements(jArray, cArray, 0);
-    }
-}
 } // namespace latinime
 #endif // LATINIME_JNI_COMMON_H
diff --git a/native/jni/src/additional_proximity_chars.cpp b/native/jni/src/additional_proximity_chars.cpp
index de87646..f594927 100644
--- a/native/jni/src/additional_proximity_chars.cpp
+++ b/native/jni/src/additional_proximity_chars.cpp
@@ -17,7 +17,9 @@
 #include "additional_proximity_chars.h"
 
 namespace latinime {
-const std::string AdditionalProximityChars::LOCALE_EN_US("en");
+// TODO: Stop using hardcoded additional proximity characters.
+// TODO: Have proximity character informations in each language's binary dictionary.
+const char *AdditionalProximityChars::LOCALE_EN_US = "en";
 
 const int32_t AdditionalProximityChars::EN_US_ADDITIONAL_A[EN_US_ADDITIONAL_A_SIZE] = {
     'e', 'i', 'o', 'u'
diff --git a/native/jni/src/additional_proximity_chars.h b/native/jni/src/additional_proximity_chars.h
index ba76cfc..1fe996d 100644
--- a/native/jni/src/additional_proximity_chars.h
+++ b/native/jni/src/additional_proximity_chars.h
@@ -17,8 +17,8 @@
 #ifndef LATINIME_ADDITIONAL_PROXIMITY_CHARS_H
 #define LATINIME_ADDITIONAL_PROXIMITY_CHARS_H
 
+#include <cstring>
 #include <stdint.h>
-#include <string>
 
 #include "defines.h"
 
@@ -27,7 +27,7 @@
 class AdditionalProximityChars {
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(AdditionalProximityChars);
-    static const std::string LOCALE_EN_US;
+    static const char *LOCALE_EN_US;
     static const int EN_US_ADDITIONAL_A_SIZE = 4;
     static const int32_t EN_US_ADDITIONAL_A[];
     static const int EN_US_ADDITIONAL_E_SIZE = 4;
@@ -39,14 +39,15 @@
     static const int EN_US_ADDITIONAL_U_SIZE = 4;
     static const int32_t EN_US_ADDITIONAL_U[];
 
-    static bool isEnLocale(const std::string *locale_str) {
-        return locale_str && locale_str->size() >= LOCALE_EN_US.size()
-                && LOCALE_EN_US.compare(0, LOCALE_EN_US.size(), *locale_str);
+    static bool isEnLocale(const char *localeStr) {
+        const size_t LOCALE_EN_US_SIZE = strlen(LOCALE_EN_US);
+        return localeStr && strlen(localeStr) >= LOCALE_EN_US_SIZE
+                && strncmp(localeStr, LOCALE_EN_US, LOCALE_EN_US_SIZE) == 0;
     }
 
  public:
-    static int getAdditionalCharsSize(const std::string *locale_str, const int32_t c) {
-        if (!isEnLocale(locale_str)) {
+    static int getAdditionalCharsSize(const char *localeStr, const int32_t c) {
+        if (!isEnLocale(localeStr)) {
             return 0;
         }
         switch(c) {
@@ -65,8 +66,8 @@
         }
     }
 
-    static const int32_t *getAdditionalChars(const std::string *locale_str, const int32_t c) {
-        if (!isEnLocale(locale_str)) {
+    static const int32_t *getAdditionalChars(const char *localeStr, const int32_t c) {
+        if (!isEnLocale(localeStr)) {
             return 0;
         }
         switch(c) {
@@ -84,10 +85,6 @@
             return 0;
         }
     }
-
-    static bool hasAdditionalChars(const std::string *locale_str, const int32_t c) {
-        return getAdditionalCharsSize(locale_str, c) > 0;
-    }
 };
 } // namespace latinime
 #endif // LATINIME_ADDITIONAL_PROXIMITY_CHARS_H
diff --git a/native/jni/src/bigram_dictionary.cpp b/native/jni/src/bigram_dictionary.cpp
index 2201711..f1d5380 100644
--- a/native/jni/src/bigram_dictionary.cpp
+++ b/native/jni/src/bigram_dictionary.cpp
@@ -60,15 +60,15 @@
         AKLOGI("Bigram: InsertAt -> %d MAX_PREDICTIONS: %d", insertAt, MAX_PREDICTIONS);
     }
     if (insertAt < MAX_PREDICTIONS) {
-        memmove((char*) bigramFreq + (insertAt + 1) * sizeof(bigramFreq[0]),
-               (char*) bigramFreq + insertAt * sizeof(bigramFreq[0]),
-               (MAX_PREDICTIONS - insertAt - 1) * sizeof(bigramFreq[0]));
+        memmove(bigramFreq + (insertAt + 1),
+                bigramFreq + insertAt,
+                (MAX_PREDICTIONS - insertAt - 1) * sizeof(bigramFreq[0]));
         bigramFreq[insertAt] = frequency;
         outputTypes[insertAt] = Dictionary::KIND_PREDICTION;
-        memmove((char*) bigramChars + (insertAt + 1) * MAX_WORD_LENGTH * sizeof(short),
-               (char*) bigramChars + (insertAt    ) * MAX_WORD_LENGTH * sizeof(short),
-               (MAX_PREDICTIONS - insertAt - 1) * sizeof(short) * MAX_WORD_LENGTH);
-        unsigned short *dest = bigramChars + (insertAt    ) * MAX_WORD_LENGTH;
+        memmove(bigramChars + (insertAt + 1) * MAX_WORD_LENGTH,
+                bigramChars + insertAt * MAX_WORD_LENGTH,
+                (MAX_PREDICTIONS - insertAt - 1) * sizeof(bigramChars[0]) * MAX_WORD_LENGTH);
+        unsigned short *dest = bigramChars + insertAt * MAX_WORD_LENGTH;
         while (length--) {
             *dest++ = *word++;
         }
diff --git a/native/jni/src/bigram_dictionary.h b/native/jni/src/bigram_dictionary.h
index d676cca..5f11ae8 100644
--- a/native/jni/src/bigram_dictionary.h
+++ b/native/jni/src/bigram_dictionary.h
@@ -29,8 +29,6 @@
     BigramDictionary(const unsigned char *dict, int maxWordLength, int maxPredictions);
     int getBigrams(const int32_t *word, int length, int *inputCodes, int codesSize,
             unsigned short *outWords, int *frequencies, int *outputTypes) const;
-    int getBigramListPositionForWord(const int32_t *prevWord, const int prevWordLength,
-            const bool forceLowerCaseSearch) const;
     void fillBigramAddressToFrequencyMapAndFilter(const int32_t *prevWord, const int prevWordLength,
             std::map<int, int> *map, uint8_t *filter) const;
     bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2) const;
@@ -45,6 +43,8 @@
     bool getFirstBitOfByte(int *pos) { return (DICT[*pos] & 0x80) > 0; }
     bool getSecondBitOfByte(int *pos) { return (DICT[*pos] & 0x40) > 0; }
     bool checkFirstCharacter(unsigned short *word, int *inputCodes) const;
+    int getBigramListPositionForWord(const int32_t *prevWord, const int prevWordLength,
+            const bool forceLowerCaseSearch) const;
 
     const unsigned char *DICT;
     const int MAX_WORD_LENGTH;
diff --git a/native/jni/src/binary_format.h b/native/jni/src/binary_format.h
index 2ee4077..d8f3e83 100644
--- a/native/jni/src/binary_format.h
+++ b/native/jni/src/binary_format.h
@@ -52,6 +52,8 @@
 
     // Mask for attribute frequency, stored on 4 bits inside the flags byte.
     static const int MASK_ATTRIBUTE_FREQUENCY = 0x0F;
+    // The numeric value of the shortcut frequency that means 'whitelist'.
+    static const int WHITELIST_SHORTCUT_FREQUENCY = 15;
 
     // Mask and flags for attribute address type selection.
     static const int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
@@ -59,13 +61,6 @@
     static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
     static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
 
- 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;
-
- public:
     const static int UNKNOWN_FORMAT = -1;
     // Originally, format version 1 had a 16-bit magic number, then the version number `01'
     // then options that must be 0. Hence the first 32-bits of the format are always as follow
@@ -92,13 +87,13 @@
     static int skipFrequency(const uint8_t flags, const int pos);
     static int skipShortcuts(const uint8_t *const dict, const uint8_t flags, const int pos);
     static int skipBigrams(const uint8_t *const dict, const uint8_t flags, const int pos);
-    static int skipAllAttributes(const uint8_t *const dict, const uint8_t flags, const int pos);
     static int skipChildrenPosAndAttributes(const uint8_t *const dict, const uint8_t flags,
             const int pos);
     static int readChildrenPosition(const uint8_t *const dict, const uint8_t flags, const int pos);
     static bool hasChildrenInFlags(const uint8_t flags);
     static int getAttributeAddressAndForwardPointer(const uint8_t *const dict, const uint8_t flags,
             int *pos);
+    static int getAttributeFrequencyFromFlags(const int flags);
     static int getTerminalPosition(const uint8_t *const root, const int32_t *const inWord,
             const int length, const bool forceLowerCaseSearch);
     static int getWordAtAddress(const uint8_t *const root, const int address, const int maxDepth,
@@ -115,6 +110,13 @@
         REQUIRES_FRENCH_LIGATURES_PROCESSING = 0x4
     };
     const static unsigned int NO_FLAGS = 0;
+
+ 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;
+    static int skipAllAttributes(const uint8_t *const dict, const uint8_t flags, const int pos);
 };
 
 inline int BinaryFormat::detectFormat(const uint8_t *const dict) {
@@ -340,6 +342,10 @@
     }
 }
 
+inline int BinaryFormat::getAttributeFrequencyFromFlags(const int flags) {
+    return flags & MASK_ATTRIBUTE_FREQUENCY;
+}
+
 // This function gets the byte position of the last chargroup of the exact matching word in the
 // dictionary. If no match is found, it returns NOT_VALID_WORD.
 inline int BinaryFormat::getTerminalPosition(const uint8_t *const root,
diff --git a/native/jni/src/char_utils.cpp b/native/jni/src/char_utils.cpp
index 45d49b0..9d886da31 100644
--- a/native/jni/src/char_utils.cpp
+++ b/native/jni/src/char_utils.cpp
@@ -885,16 +885,16 @@
 };
 
 static int compare_pair_capital(const void *a, const void *b) {
-    return (int)(*(unsigned short *)a)
-            - (int)((struct LatinCapitalSmallPair*)b)->capital;
+    return static_cast<int>(*static_cast<const unsigned short *>(a))
+            - static_cast<int>((static_cast<const struct LatinCapitalSmallPair *>(b))->capital);
 }
 
-unsigned short latin_tolower(unsigned short c) {
+unsigned short latin_tolower(const unsigned short c) {
     struct LatinCapitalSmallPair *p =
-            (struct LatinCapitalSmallPair *)bsearch(&c, SORTED_CHAR_MAP,
+            static_cast<struct LatinCapitalSmallPair *>(bsearch(&c, SORTED_CHAR_MAP,
                     sizeof(SORTED_CHAR_MAP) / sizeof(SORTED_CHAR_MAP[0]),
                     sizeof(SORTED_CHAR_MAP[0]),
-                    compare_pair_capital);
+                    compare_pair_capital));
     return p ? p->small : c;
 }
 } // namespace latinime
diff --git a/native/jni/src/char_utils.h b/native/jni/src/char_utils.h
index edd96bb..b30677f 100644
--- a/native/jni/src/char_utils.h
+++ b/native/jni/src/char_utils.h
@@ -17,21 +17,23 @@
 #ifndef LATINIME_CHAR_UTILS_H
 #define LATINIME_CHAR_UTILS_H
 
+#include <cctype>
+
 namespace latinime {
 
-inline static int isAsciiUpper(unsigned short c) {
-    return c >= 'A' && c <= 'Z';
+inline static bool isAsciiUpper(unsigned short c) {
+    return isupper(static_cast<int>(c)) != 0;
 }
 
 inline static unsigned short toAsciiLower(unsigned short c) {
     return c - 'A' + 'a';
 }
 
-inline static int isAscii(unsigned short c) {
-    return c <= 127;
+inline static bool isAscii(unsigned short c) {
+    return isascii(static_cast<int>(c)) != 0;
 }
 
-unsigned short latin_tolower(unsigned short c);
+unsigned short latin_tolower(const unsigned short c);
 
 /**
  * Table mapping most combined Latin, Greek, and Cyrillic characters
diff --git a/native/jni/src/correction.cpp b/native/jni/src/correction.cpp
index ea4bdda..9ad65b0 100644
--- a/native/jni/src/correction.cpp
+++ b/native/jni/src/correction.cpp
@@ -61,19 +61,19 @@
 }
 
 inline static void calcEditDistanceOneStep(int *editDistanceTable, const unsigned short *input,
-        const int inputLength, const unsigned short *output, const int outputLength) {
+        const int inputSize, const unsigned short *output, const int outputLength) {
     // TODO: Make sure that editDistance[0 ~ MAX_WORD_LENGTH_INTERNAL] is not touched.
-    // Let dp[i][j] be editDistanceTable[i * (inputLength + 1) + j].
-    // Assuming that dp[0][0] ... dp[outputLength - 1][inputLength] are already calculated,
-    // and calculate dp[ouputLength][0] ... dp[outputLength][inputLength].
-    int *const current = editDistanceTable + outputLength * (inputLength + 1);
-    const int *const prev = editDistanceTable + (outputLength - 1) * (inputLength + 1);
+    // Let dp[i][j] be editDistanceTable[i * (inputSize + 1) + j].
+    // Assuming that dp[0][0] ... dp[outputLength - 1][inputSize] are already calculated,
+    // and calculate dp[ouputLength][0] ... dp[outputLength][inputSize].
+    int *const current = editDistanceTable + outputLength * (inputSize + 1);
+    const int *const prev = editDistanceTable + (outputLength - 1) * (inputSize + 1);
     const int *const prevprev =
-            outputLength >= 2 ? editDistanceTable + (outputLength - 2) * (inputLength + 1) : 0;
+            outputLength >= 2 ? editDistanceTable + (outputLength - 2) * (inputSize + 1) : 0;
     current[0] = outputLength;
     const uint32_t co = toBaseLowerCase(output[outputLength - 1]);
     const uint32_t prevCO = outputLength >= 2 ? toBaseLowerCase(output[outputLength - 2]) : 0;
-    for (int i = 1; i <= inputLength; ++i) {
+    for (int i = 1; i <= inputSize; ++i) {
         const uint32_t ci = toBaseLowerCase(input[i - 1]);
         const uint16_t cost = (ci == co) ? 0 : 1;
         current[i] = min(current[i - 1] + 1, min(prev[i] + 1, prev[i - 1] + cost));
@@ -84,11 +84,11 @@
 }
 
 inline static int getCurrentEditDistance(int *editDistanceTable, const int editDistanceTableWidth,
-        const int outputLength, const int inputLength) {
+        const int outputLength, const int inputSize) {
     if (DEBUG_EDIT_DISTANCE) {
-        AKLOGI("getCurrentEditDistance %d, %d", inputLength, outputLength);
+        AKLOGI("getCurrentEditDistance %d, %d", inputSize, outputLength);
     }
-    return editDistanceTable[(editDistanceTableWidth + 1) * (outputLength) + inputLength];
+    return editDistanceTable[(editDistanceTableWidth + 1) * (outputLength) + inputSize];
 }
 
 //////////////////////
@@ -109,12 +109,12 @@
     mTotalTraverseCount = 0;
 }
 
-void Correction::initCorrection(const ProximityInfo *pi, const int inputLength,
+void Correction::initCorrection(const ProximityInfo *pi, const int inputSize,
         const int maxDepth) {
     mProximityInfo = pi;
-    mInputLength = inputLength;
+    mInputSize = inputSize;
     mMaxDepth = maxDepth;
-    mMaxEditDistance = mInputLength < 5 ? 2 : mInputLength / 2;
+    mMaxEditDistance = mInputSize < 5 ? 2 : mInputSize / 2;
     // TODO: This is not supposed to be required.  Check what's going wrong with
     // editDistance[0 ~ MAX_WORD_LENGTH_INTERNAL]
     initEditDistance(mEditDistanceTable);
@@ -154,11 +154,13 @@
         if (mSkipPos >= 0) ++inputCount;
         if (mExcessivePos >= 0) ++inputCount;
         if (mTransposedPos >= 0) ++inputCount;
-        // TODO: remove this assert
-        assert(inputCount <= 1);
     }
 }
 
+bool Correction::sameAsTyped() {
+    return mProximityInfoState.sameAsTyped(mWord, mOutputIndex);
+}
+
 int Correction::getFreqForSplitMultipleWords(const int *freqArray, const int *wordLengthArray,
         const int wordCount, const bool isSpaceProximity, const unsigned short *word) {
     return Correction::RankingAlgorithm::calcFreqForSplitMultipleWords(freqArray, wordLengthArray,
@@ -166,26 +168,22 @@
 }
 
 int Correction::getFinalProbability(const int probability, unsigned short **word, int *wordLength) {
-    return getFinalProbabilityInternal(probability, word, wordLength, mInputLength);
+    return getFinalProbabilityInternal(probability, word, wordLength, mInputSize);
 }
 
 int Correction::getFinalProbabilityForSubQueue(const int probability, unsigned short **word,
-        int *wordLength, const int inputLength) {
-    return getFinalProbabilityInternal(probability, word, wordLength, inputLength);
+        int *wordLength, const int inputSize) {
+    return getFinalProbabilityInternal(probability, word, wordLength, inputSize);
 }
 
 int Correction::getFinalProbabilityInternal(const int probability, unsigned short **word,
-        int *wordLength, const int inputLength) {
+        int *wordLength, const int inputSize) {
     const int outputIndex = mTerminalOutputIndex;
     const int inputIndex = mTerminalInputIndex;
     *wordLength = outputIndex + 1;
-    if (outputIndex < MIN_SUGGEST_DEPTH) {
-        return NOT_A_PROBABILITY;
-    }
-
     *word = mWord;
     int finalProbability= Correction::RankingAlgorithm::calculateFinalProbability(
-            inputIndex, outputIndex, probability, mEditDistanceTable, this, inputLength);
+            inputIndex, outputIndex, probability, mEditDistanceTable, this, inputSize);
     return finalProbability;
 }
 
@@ -228,7 +226,7 @@
 }
 
 // TODO: remove
-int Correction::getInputIndex() {
+int Correction::getInputIndex() const {
     return mInputIndex;
 }
 
@@ -272,13 +270,13 @@
     // TODO: use edit distance here
     return mOutputIndex - 1 >= mMaxDepth || mProximityCount > mMaxEditDistance
             // Allow one char longer word for missing character
-            || (!mDoAutoCompletion && (mOutputIndex > mInputLength));
+            || (!mDoAutoCompletion && (mOutputIndex > mInputSize));
 }
 
 void Correction::addCharToCurrentWord(const int32_t c) {
     mWord[mOutputIndex] = c;
     const unsigned short *primaryInputWord = mProximityInfoState.getPrimaryInputWord();
-    calcEditDistanceOneStep(mEditDistanceTable, primaryInputWord, mInputLength,
+    calcEditDistanceOneStep(mEditDistanceTable, primaryInputWord, mInputSize,
             mWord, mOutputIndex + 1);
 }
 
@@ -327,7 +325,7 @@
     // Skip checking this node
     if (mNeedsToTraverseAllNodes || isSingleQuote(c)) {
         bool incremented = false;
-        if (mLastCharExceeded && mInputIndex == mInputLength - 1) {
+        if (mLastCharExceeded && mInputIndex == mInputSize - 1) {
             // TODO: Do not check the proximity if EditDistance exceeds the threshold
             const ProximityType matchId = mProximityInfoState.getMatchedProximityId(
                     mInputIndex, c, true, &proximityIndex);
@@ -356,7 +354,7 @@
         if (mExcessiveCount == 0 && mExcessivePos < mOutputIndex) {
             mExcessivePos = mOutputIndex;
         }
-        if (mExcessivePos < mInputLength - 1) {
+        if (mExcessivePos < mInputSize - 1) {
             mExceeding = mExcessivePos == mInputIndex && canTryCorrection;
         }
     }
@@ -375,7 +373,7 @@
         if (mTransposedCount == 0 && mTransposedPos < mOutputIndex) {
             mTransposedPos = mOutputIndex;
         }
-        if (mTransposedPos < mInputLength - 1) {
+        if (mTransposedPos < mInputSize - 1) {
             mTransposing = mInputIndex == mTransposedPos && canTryCorrection;
         }
     }
@@ -394,7 +392,7 @@
         } else {
             --mTransposedCount;
             if (DEBUG_CORRECTION
-                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                     && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
                             || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 DUMP_WORD(mWord, mOutputIndex);
@@ -425,7 +423,7 @@
                 && isEquivalentChar(mProximityInfoState.getMatchedProximityId(
                         mInputIndex, mWord[mOutputIndex - 1], false))) {
             if (DEBUG_CORRECTION
-                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                     && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
                             || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 AKLOGI("CONVERSION p->e %c", mWord[mOutputIndex - 1]);
@@ -455,7 +453,7 @@
         // As the current char turned out to be an unrelated char,
         // we will try other correction-types. Please note that mCorrectionStates[mOutputIndex]
         // here refers to the previous state.
-        if (mInputIndex < mInputLength - 1 && mOutputIndex > 0 && mTransposedCount > 0
+        if (mInputIndex < mInputSize - 1 && mOutputIndex > 0 && mTransposedCount > 0
                 && !mCorrectionStates[mOutputIndex].mTransposing
                 && mCorrectionStates[mOutputIndex - 1].mTransposing
                 && isEquivalentChar(mProximityInfoState.getMatchedProximityId(
@@ -492,7 +490,7 @@
             ++mSkippedCount;
             --mProximityCount;
             return processSkipChar(c, isTerminal, false);
-        } else if (mInputIndex - 1 < mInputLength
+        } else if (mInputIndex - 1 < mInputSize
                 && mSkippedCount > 0
                 && mCorrectionStates[mOutputIndex].mSkipping
                 && mCorrectionStates[mOutputIndex].mAdditionalProximityMatching
@@ -504,7 +502,7 @@
             mProximityMatching = true;
             ++mProximityCount;
             mDistances[mOutputIndex] = ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO;
-        } else if ((mExceeding || mTransposing) && mInputIndex - 1 < mInputLength
+        } else if ((mExceeding || mTransposing) && mInputIndex - 1 < mInputSize
                 && isEquivalentChar(
                         mProximityInfoState.getMatchedProximityId(mInputIndex + 1, c, false))) {
             // 1.2. Excessive or transpose correction
@@ -515,7 +513,7 @@
                 incrementInputIndex();
             }
             if (DEBUG_CORRECTION
-                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                     && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
                             || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 DUMP_WORD(mWord, mOutputIndex);
@@ -531,7 +529,7 @@
             // 3. Skip correction
             ++mSkippedCount;
             if (DEBUG_CORRECTION
-                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                     && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
                             || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 AKLOGI("SKIP: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
@@ -544,7 +542,7 @@
             ++mProximityCount;
             mDistances[mOutputIndex] = ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO;
             if (DEBUG_CORRECTION
-                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                     && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
                             || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 AKLOGI("ADDITIONALPROX: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
@@ -552,7 +550,7 @@
             }
         } else {
             if (DEBUG_CORRECTION
-                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                     && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
                             || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 DUMP_WORD(mWord, mOutputIndex);
@@ -562,7 +560,7 @@
             return processUnrelatedCorrectionType();
         }
     } else if (secondTransposing) {
-        // If inputIndex is greater than mInputLength, that means there is no
+        // If inputIndex is greater than mInputSize, that means there is no
         // proximity chars. So, we don't need to check proximity.
         mMatching = true;
     } else if (isEquivalentChar(matchedProximityCharId)) {
@@ -575,7 +573,7 @@
         mDistances[mOutputIndex] =
                 mProximityInfoState.getNormalizedSquaredDistance(mInputIndex, proximityIndex);
         if (DEBUG_CORRECTION
-                && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                 && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
                         || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
             AKLOGI("PROX: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
@@ -587,8 +585,8 @@
 
     // 4. Last char excessive correction
     mLastCharExceeded = mExcessiveCount == 0 && mSkippedCount == 0 && mTransposedCount == 0
-            && mProximityCount == 0 && (mInputIndex == mInputLength - 2);
-    const bool isSameAsUserTypedLength = (mInputLength == mInputIndex + 1) || mLastCharExceeded;
+            && mProximityCount == 0 && (mInputIndex == mInputSize - 2);
+    const bool isSameAsUserTypedLength = (mInputSize == mInputIndex + 1) || mLastCharExceeded;
     if (mLastCharExceeded) {
         ++mExcessiveCount;
     }
@@ -599,7 +597,7 @@
     }
 
     const bool needsToTryOnTerminalForTheLastPossibleExcessiveChar =
-            mExceeding && mInputIndex == mInputLength - 2;
+            mExceeding && mInputIndex == mInputSize - 2;
 
     // Finally, we are ready to go to the next character, the next "virtual node".
     // We should advance the input index.
@@ -615,7 +613,7 @@
         mTerminalInputIndex = mInputIndex - 1;
         mTerminalOutputIndex = mOutputIndex - 1;
         if (DEBUG_CORRECTION
-                && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputSize)
                 && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
             DUMP_WORD(mWord, mOutputIndex);
             AKLOGI("ONTERMINAL(1): %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
@@ -629,9 +627,6 @@
     }
 }
 
-Correction::~Correction() {
-}
-
 inline static int getQuoteCount(const unsigned short *word, const int length) {
     int quoteCount = 0;
     for (int i = 0; i < length; ++i) {
@@ -653,7 +648,7 @@
 /* static */
 int Correction::RankingAlgorithm::calculateFinalProbability(const int inputIndex,
         const int outputIndex, const int freq, int *editDistanceTable, const Correction *correction,
-        const int inputLength) {
+        const int inputSize) {
     const int excessivePos = correction->getExcessivePos();
     const int typedLetterMultiplier = correction->TYPED_LETTER_MULTIPLIER;
     const int fullWordMultiplier = correction->FULL_WORD_MULTIPLIER;
@@ -665,55 +660,55 @@
     const bool lastCharExceeded = correction->mLastCharExceeded;
     const bool useFullEditDistance = correction->mUseFullEditDistance;
     const int outputLength = outputIndex + 1;
-    if (skippedCount >= inputLength || inputLength == 0) {
+    if (skippedCount >= inputSize || inputSize == 0) {
         return -1;
     }
 
     // TODO: find more robust way
-    bool sameLength = lastCharExceeded ? (inputLength == inputIndex + 2)
-            : (inputLength == inputIndex + 1);
+    bool sameLength = lastCharExceeded ? (inputSize == inputIndex + 2)
+            : (inputSize == inputIndex + 1);
 
     // TODO: use mExcessiveCount
-    const int matchCount = inputLength - correction->mProximityCount - excessiveCount;
+    const int matchCount = inputSize - correction->mProximityCount - excessiveCount;
 
     const unsigned short *word = correction->mWord;
     const bool skipped = skippedCount > 0;
 
     const int quoteDiffCount = max(0, getQuoteCount(word, outputLength)
-            - getQuoteCount(proximityInfoState->getPrimaryInputWord(), inputLength));
+            - getQuoteCount(proximityInfoState->getPrimaryInputWord(), inputSize));
 
     // TODO: Calculate edit distance for transposed and excessive
     int ed = 0;
     if (DEBUG_DICT_FULL) {
-        dumpEditDistance10ForDebug(editDistanceTable, correction->mInputLength, outputLength);
+        dumpEditDistance10ForDebug(editDistanceTable, correction->mInputSize, outputLength);
     }
     int adjustedProximityMatchedCount = proximityMatchedCount;
 
     int finalFreq = freq;
 
     if (DEBUG_CORRECTION_FREQ
-            && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputLength)) {
+            && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputSize)) {
         AKLOGI("FinalFreq0: %d", finalFreq);
     }
     // TODO: Optimize this.
     if (transposedCount > 0 || proximityMatchedCount > 0 || skipped || excessiveCount > 0) {
-        ed = getCurrentEditDistance(editDistanceTable, correction->mInputLength, outputLength,
-                inputLength) - transposedCount;
+        ed = getCurrentEditDistance(editDistanceTable, correction->mInputSize, outputLength,
+                inputSize) - transposedCount;
 
         const int matchWeight = powerIntCapped(typedLetterMultiplier,
-                max(inputLength, outputLength) - ed);
+                max(inputSize, outputLength) - ed);
         multiplyIntCapped(matchWeight, &finalFreq);
 
         // TODO: Demote further if there are two or more excessive chars with longer user input?
-        if (inputLength > outputLength) {
+        if (inputSize > outputLength) {
             multiplyRate(INPUT_EXCEEDS_OUTPUT_DEMOTION_RATE, &finalFreq);
         }
 
         ed = max(0, ed - quoteDiffCount);
-        adjustedProximityMatchedCount = min(max(0, ed - (outputLength - inputLength)),
+        adjustedProximityMatchedCount = min(max(0, ed - (outputLength - inputSize)),
                 proximityMatchedCount);
         if (transposedCount <= 0) {
-            if (ed == 1 && (inputLength == outputLength - 1 || inputLength == outputLength + 1)) {
+            if (ed == 1 && (inputSize == outputLength - 1 || inputSize == outputLength + 1)) {
                 // Promote a word with just one skipped or excessive char
                 if (sameLength) {
                     multiplyRate(WORDS_WITH_JUST_ONE_CORRECTION_PROMOTION_RATE
@@ -742,8 +737,8 @@
     // Demotion for a word with missing character
     if (skipped) {
         const int demotionRate = WORDS_WITH_MISSING_CHARACTER_DEMOTION_RATE
-                * (10 * inputLength - WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X)
-                / (10 * inputLength
+                * (10 * inputSize - WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X)
+                / (10 * inputSize
                         - WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X + 10);
         if (DEBUG_DICT_FULL) {
             AKLOGI("Demotion rate for missing character is %d.", demotionRate);
@@ -845,7 +840,7 @@
             ? adjustedProximityMatchedCount
             : (proximityMatchedCount + transposedCount);
     multiplyRate(
-            100 - CORRECTION_COUNT_RATE_DEMOTION_RATE_BASE * errorCount / inputLength, &finalFreq);
+            100 - CORRECTION_COUNT_RATE_DEMOTION_RATE_BASE * errorCount / inputSize, &finalFreq);
 
     // Promotion for an exactly matched word
     if (ed == 0) {
@@ -880,7 +875,7 @@
          e ... exceeding
          p ... proximity matching
      */
-    if (matchCount == inputLength && matchCount >= 2 && !skipped
+    if (matchCount == inputSize && matchCount >= 2 && !skipped
             && word[matchCount] == word[matchCount - 1]) {
         multiplyRate(WORDS_WITH_MATCH_SKIP_PROMOTION_RATE, &finalFreq);
     }
@@ -890,8 +885,8 @@
         multiplyIntCapped(fullWordMultiplier, &finalFreq);
     }
 
-    if (useFullEditDistance && outputLength > inputLength + 1) {
-        const int diff = outputLength - inputLength - 1;
+    if (useFullEditDistance && outputLength > inputSize + 1) {
+        const int diff = outputLength - inputSize - 1;
         const int divider = diff < 31 ? 1 << diff : S_INT_MAX;
         finalFreq = divider > finalFreq ? 1 : finalFreq / divider;
     }
@@ -901,8 +896,8 @@
     }
 
     if (DEBUG_CORRECTION_FREQ
-            && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputLength)) {
-        DUMP_WORD(correction->getPrimaryInputWord(), inputLength);
+            && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputSize)) {
+        DUMP_WORD(correction->getPrimaryInputWord(), inputSize);
         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,
@@ -1094,7 +1089,7 @@
 // In dictionary.cpp, getSuggestion() method,
 // suggestion scores are computed using the below formula.
 // original score
-//  := pow(mTypedLetterMultiplier (this is defined 2),
+//  := powf(mTypedLetterMultiplier (this is defined 2),
 //         (the number of matched characters between typed word and suggested word))
 //     * (individual word's score which defined in the unigram dictionary,
 //         and this score is defined in range [0, 255].)
@@ -1106,11 +1101,11 @@
 //       capitalization, then treat it as if the score was 255.
 //     - If before.length() == after.length()
 //       => multiply by mFullWordMultiplier (this is defined 2))
-// So, maximum original score is pow(2, min(before.length(), after.length())) * 255 * 2 * 1.2
+// So, maximum original score is powf(2, min(before.length(), after.length())) * 255 * 2 * 1.2
 // For historical reasons we ignore the 1.2 modifier (because the measure for a good
 // autocorrection threshold was done at a time when it didn't exist). This doesn't change
 // the result.
-// So, we can normalize original score by dividing pow(2, min(b.l(),a.l())) * 255 * 2.
+// So, we can normalize original score by dividing powf(2, min(b.l(),a.l())) * 255 * 2.
 
 /* static */
 float Correction::RankingAlgorithm::calcNormalizedScore(const unsigned short *before,
@@ -1132,7 +1127,7 @@
     }
 
     const float maxScore = score >= S_INT_MAX ? S_INT_MAX : MAX_INITIAL_SCORE
-            * pow(static_cast<float>(TYPED_LETTER_MULTIPLIER),
+            * powf(static_cast<float>(TYPED_LETTER_MULTIPLIER),
                     static_cast<float>(min(beforeLength, afterLength - spaceCount)))
             * FULL_WORD_MULTIPLIER;
 
diff --git a/native/jni/src/correction.h b/native/jni/src/correction.h
index 81623a4..f016d54 100644
--- a/native/jni/src/correction.h
+++ b/native/jni/src/correction.h
@@ -18,6 +18,7 @@
 #define LATINIME_CORRECTION_H
 
 #include <cassert>
+#include <cstring> // for memset()
 #include <stdint.h>
 
 #include "correction_state.h"
@@ -38,10 +39,108 @@
         NOT_ON_TERMINAL
     } CorrectionType;
 
+    Correction()
+            : mProximityInfo(0), mUseFullEditDistance(false), mDoAutoCompletion(false),
+              mMaxEditDistance(0), mMaxDepth(0), mInputSize(0), mSpaceProximityPos(0),
+              mMissingSpacePos(0), mTerminalInputIndex(0), mTerminalOutputIndex(0), mMaxErrors(0),
+              mTotalTraverseCount(0), mNeedsToTraverseAllNodes(false), mOutputIndex(0),
+              mInputIndex(0), mEquivalentCharCount(0), mProximityCount(0), mExcessiveCount(0),
+              mTransposedCount(0), mSkippedCount(0), mTransposedPos(0), mExcessivePos(0),
+              mSkipPos(0), mLastCharExceeded(false), mMatching(false), mProximityMatching(false),
+              mAdditionalProximityMatching(false), mExceeding(false), mTransposing(false),
+              mSkipping(false), mProximityInfoState() {
+        memset(mWord, 0, sizeof(mWord));
+        memset(mDistances, 0, sizeof(mDistances));
+        memset(mEditDistanceTable, 0, sizeof(mEditDistanceTable));
+        // NOTE: mCorrectionStates is an array of instances.
+        // No need to initialize it explicitly here.
+    }
+
+    virtual ~Correction() {}
+    void resetCorrection();
+    void initCorrection(
+            const ProximityInfo *pi, const int inputSize, const int maxWordLength);
+    void initCorrectionState(const int rootPos, const int childCount, const bool traverseAll);
+
+    // TODO: remove
+    void setCorrectionParams(const int skipPos, const int excessivePos, const int transposedPos,
+            const int spaceProximityPos, const int missingSpacePos, const bool useFullEditDistance,
+            const bool doAutoCompletion, const int maxErrors);
+    void checkState();
+    bool sameAsTyped();
+    bool initProcessState(const int index);
+
+    int getInputIndex() const;
+
+    bool needsToPrune() const;
+
+    int pushAndGetTotalTraverseCount() {
+        return ++mTotalTraverseCount;
+    }
+
+    int getFreqForSplitMultipleWords(
+            const int *freqArray, const int *wordLengthArray, const int wordCount,
+            const bool isSpaceProximity, const unsigned short *word);
+    int getFinalProbability(const int probability, unsigned short **word, int *wordLength);
+    int getFinalProbabilityForSubQueue(const int probability, unsigned short **word,
+            int *wordLength, const int inputSize);
+
+    CorrectionType processCharAndCalcState(const int32_t c, const bool isTerminal);
+
+    /////////////////////////
+    // Tree helper methods
+    int goDownTree(const int parentIndex, const int childCount, const int firstChildPos);
+
+    inline int getTreeSiblingPos(const int index) const {
+        return mCorrectionStates[index].mSiblingPos;
+    }
+
+    inline void setTreeSiblingPos(const int index, const int pos) {
+        mCorrectionStates[index].mSiblingPos = pos;
+    }
+
+    inline int getTreeParentIndex(const int index) const {
+        return mCorrectionStates[index].mParentIndex;
+    }
+
+    class RankingAlgorithm {
+     public:
+        static int calculateFinalProbability(const int inputIndex, const int depth,
+                const int probability, int *editDistanceTable, const Correction *correction,
+                const int inputSize);
+        static int calcFreqForSplitMultipleWords(const int *freqArray, const int *wordLengthArray,
+                const int wordCount, const Correction *correction, const bool isSpaceProximity,
+                const unsigned short *word);
+        static float calcNormalizedScore(const unsigned short *before, const int beforeLength,
+                const unsigned short *after, const int afterLength, const int score);
+        static int editDistance(const unsigned short *before,
+                const int beforeLength, const unsigned short *after, const int afterLength);
+     private:
+        static const int CODE_SPACE = ' ';
+        static const int MAX_INITIAL_SCORE = 255;
+    };
+
+    // proximity info state
+    void initInputParams(const ProximityInfo *proximityInfo, const int32_t *inputCodes,
+            const int inputSize, const int *xCoordinates, const int *yCoordinates) {
+        mProximityInfoState.initInputParams(0, MAX_POINT_TO_KEY_LENGTH,
+                proximityInfo, inputCodes, inputSize, xCoordinates, yCoordinates, 0, 0, false);
+    }
+
+    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);
+
     /////////////////////////
     // static inline utils //
     /////////////////////////
-
     static const int TWO_31ST_DIV_255 = S_INT_MAX / 255;
     static inline int capped255MultForFullMatchAccentsOrCapitalizationDifference(const int num) {
         return (num < TWO_31ST_DIV_255 ? 255 * num : S_INT_MAX);
@@ -94,106 +193,25 @@
         }
     }
 
-    Correction() {};
-    void resetCorrection();
-    void initCorrection(
-            const ProximityInfo *pi, const int inputLength, const int maxWordLength);
-    void initCorrectionState(const int rootPos, const int childCount, const bool traverseAll);
-
-    // TODO: remove
-    void setCorrectionParams(const int skipPos, const int excessivePos, const int transposedPos,
-            const int spaceProximityPos, const int missingSpacePos, const bool useFullEditDistance,
-            const bool doAutoCompletion, const int maxErrors);
-    void checkState();
-    bool initProcessState(const int index);
-
-    int getInputIndex();
-
-    virtual ~Correction();
-    int getSpaceProximityPos() const {
+    inline int getSpaceProximityPos() const {
         return mSpaceProximityPos;
     }
-    int getMissingSpacePos() const {
+    inline int getMissingSpacePos() const {
         return mMissingSpacePos;
     }
 
-    int getSkipPos() const {
+    inline int getSkipPos() const {
         return mSkipPos;
     }
 
-    int getExcessivePos() const {
+    inline int getExcessivePos() const {
         return mExcessivePos;
     }
 
-    int getTransposedPos() const {
+    inline int getTransposedPos() const {
         return mTransposedPos;
     }
 
-    bool needsToPrune() const;
-
-    int pushAndGetTotalTraverseCount() {
-        return ++mTotalTraverseCount;
-    }
-
-    int getFreqForSplitMultipleWords(
-            const int *freqArray, const int *wordLengthArray, const int wordCount,
-            const bool isSpaceProximity, const unsigned short *word);
-    int getFinalProbability(const int probability, unsigned short **word, int *wordLength);
-    int getFinalProbabilityForSubQueue(const int probability, unsigned short **word,
-            int *wordLength, const int inputLength);
-
-    CorrectionType processCharAndCalcState(const int32_t c, const bool isTerminal);
-
-    /////////////////////////
-    // Tree helper methods
-    int goDownTree(const int parentIndex, const int childCount, const int firstChildPos);
-
-    inline int getTreeSiblingPos(const int index) const {
-        return mCorrectionStates[index].mSiblingPos;
-    }
-
-    inline void setTreeSiblingPos(const int index, const int pos) {
-        mCorrectionStates[index].mSiblingPos = pos;
-    }
-
-    inline int getTreeParentIndex(const int index) const {
-        return mCorrectionStates[index].mParentIndex;
-    }
-
-    class RankingAlgorithm {
-     public:
-        static int calculateFinalProbability(const int inputIndex, const int depth,
-                const int probability, int *editDistanceTable, const Correction *correction,
-                const int inputLength);
-        static int calcFreqForSplitMultipleWords(const int *freqArray, const int *wordLengthArray,
-                const int wordCount, const Correction *correction, const bool isSpaceProximity,
-                const unsigned short *word);
-        static float calcNormalizedScore(const unsigned short *before, const int beforeLength,
-                const unsigned short *after, const int afterLength, const int score);
-        static int editDistance(const unsigned short *before,
-                const int beforeLength, const unsigned short *after, const int afterLength);
-     private:
-        static const int CODE_SPACE = ' ';
-        static const int MAX_INITIAL_SCORE = 255;
-    };
-
-    // 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();
@@ -203,7 +221,7 @@
     inline CorrectionType processUnrelatedCorrectionType();
     inline void addCharToCurrentWord(const int32_t c);
     inline int getFinalProbabilityInternal(const int probability, unsigned short **word,
-            int *wordLength, const int inputLength);
+            int *wordLength, const int inputSize);
 
     static const int TYPED_LETTER_MULTIPLIER = 2;
     static const int FULL_WORD_MULTIPLIER = 2;
@@ -213,7 +231,7 @@
     bool mDoAutoCompletion;
     int mMaxEditDistance;
     int mMaxDepth;
-    int mInputLength;
+    int mInputSize;
     int mSpaceProximityPos;
     int mMissingSpacePos;
     int mTerminalInputIndex;
diff --git a/native/jni/src/debug.h b/native/jni/src/debug.h
index 2168d66..8f6b69d 100644
--- a/native/jni/src/debug.h
+++ b/native/jni/src/debug.h
@@ -22,7 +22,7 @@
 static inline unsigned char *convertToUnibyteString(unsigned short *input, unsigned char *output,
         const unsigned int length) {
     unsigned int i = 0;
-    for (; i <= length && input[i] != 0; ++i)
+    for (; i < length && input[i] != 0; ++i)
         output[i] = input[i] & 0xFF;
     output[i] = 0;
     return output;
@@ -31,7 +31,7 @@
 static inline unsigned char *convertToUnibyteStringAndReplaceLastChar(unsigned short *input,
         unsigned char *output, const unsigned int length, unsigned char c) {
     unsigned int i = 0;
-    for (; i <= length && input[i] != 0; ++i)
+    for (; i < length && input[i] != 0; ++i)
         output[i] = input[i] & 0xFF;
     if (i > 0) output[i-1] = c;
     output[i] = 0;
@@ -58,11 +58,12 @@
 }
 
 static inline void printDebug(const char *tag, int *codes, int codesSize, int MAX_PROXIMITY_CHARS) {
-    unsigned char *buf = (unsigned char*)malloc((1 + codesSize) * sizeof(*buf));
+    unsigned char *buf = static_cast<unsigned char *>(malloc((1 + codesSize) * sizeof(*buf)));
 
     buf[codesSize] = 0;
-    while (--codesSize >= 0)
-        buf[codesSize] = (unsigned char)codes[codesSize * MAX_PROXIMITY_CHARS];
+    while (--codesSize >= 0) {
+        buf[codesSize] = static_cast<unsigned char>(codes[codesSize * MAX_PROXIMITY_CHARS]);
+    }
     AKLOGI("%s, WORD = %s", tag, buf);
 
     free(buf);
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index 31dd61e..9b53007 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -265,6 +265,9 @@
 // This must be equal to ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE in KeyDetector.java
 #define ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE 2
 
+// Assuming locale strings such as en_US, sr-Latn etc.
+#define MAX_LOCALE_STRING_LENGTH 10
+
 // Word limit for sub queues used in WordsPriorityQueuePool.  Sub queues are temporary queues used
 // for better performance.
 // Holds up to 1 candidate for each word
@@ -291,12 +294,13 @@
 
 #define MAX_SPACES_INTERNAL 16
 
+// Max Distance between point to key
+#define MAX_POINT_TO_KEY_LENGTH 10000000
+
 // TODO: Reduce this constant if possible; check the maximum number of digraphs in the same
 // word in the dictionary for languages with digraphs, like German and French
 #define DEFAULT_MAX_DIGRAPH_SEARCH_DEPTH 5
 
-// Minimum suggest depth for one word for all cases except for missing space suggestions.
-#define MIN_SUGGEST_DEPTH 1
 #define MIN_USER_TYPED_LENGTH_FOR_MULTIPLE_WORD_SUGGESTION 3
 #define MIN_USER_TYPED_LENGTH_FOR_EXCESSIVE_CHARACTER_SUGGESTION 3
 
diff --git a/native/jni/com_android_inputmethod_latin_NativeUtils.h b/native/jni/src/dic_traverse_wrapper.cpp
similarity index 64%
copy from native/jni/com_android_inputmethod_latin_NativeUtils.h
copy to native/jni/src/dic_traverse_wrapper.cpp
index d1ffb8f..88ca9fa 100644
--- a/native/jni/com_android_inputmethod_latin_NativeUtils.h
+++ b/native/jni/src/dic_traverse_wrapper.cpp
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-#ifndef _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
-#define _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
+#define LOG_TAG "LatinIME: jni: Session"
 
-#include "jni.h"
+#include "dic_traverse_wrapper.h"
 
 namespace latinime {
-
-int register_NativeUtils(JNIEnv *env);
-
+void *(*DicTraverseWrapper::sDicTraverseSessionFactoryMethod)(JNIEnv *, jstring) = 0;
+void (*DicTraverseWrapper::sDicTraverseSessionReleaseMethod)(void *) = 0;
+void (*DicTraverseWrapper::sDicTraverseSessionInitMethod)(
+        void *, const Dictionary *const, const int *, const int) = 0;
 } // namespace latinime
-#endif // _COM_ANDROID_INPUTMETHOD_LATIN_NATIVEUTILS_H
diff --git a/native/jni/src/dic_traverse_wrapper.h b/native/jni/src/dic_traverse_wrapper.h
new file mode 100644
index 0000000..2923824
--- /dev/null
+++ b/native/jni/src/dic_traverse_wrapper.h
@@ -0,0 +1,67 @@
+/*
+ * 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_DIC_TRAVERSE_WRAPPER_H
+#define LATINIME_DIC_TRAVERSE_WRAPPER_H
+
+#include <stdint.h>
+
+#include "defines.h"
+#include "jni.h"
+
+namespace latinime {
+class Dictionary;
+// TODO: Remove
+class DicTraverseWrapper {
+ public:
+    static void *getDicTraverseSession(JNIEnv *env, jstring locale) {
+        if (sDicTraverseSessionFactoryMethod) {
+            return sDicTraverseSessionFactoryMethod(env, locale);
+        }
+        return 0;
+    }
+    static void initDicTraverseSession(void *traverseSession,
+            const Dictionary *const dictionary, const int *prevWord, const int prevWordLength) {
+        if (sDicTraverseSessionInitMethod) {
+            sDicTraverseSessionInitMethod(traverseSession, dictionary, prevWord, prevWordLength);
+        }
+    }
+    static void releaseDicTraverseSession(void *traverseSession) {
+        if (sDicTraverseSessionReleaseMethod) {
+            sDicTraverseSessionReleaseMethod(traverseSession);
+        }
+    }
+    static void setTraverseSessionFactoryMethod(
+            void *(*factoryMethod)(JNIEnv *, jstring)) {
+        sDicTraverseSessionFactoryMethod = factoryMethod;
+    }
+    static void setTraverseSessionInitMethod(
+            void (*initMethod)(void *, const Dictionary *const, const int *, const int)) {
+        sDicTraverseSessionInitMethod = initMethod;
+    }
+    static void setTraverseSessionReleaseMethod(void (*releaseMethod)(void *)) {
+        sDicTraverseSessionReleaseMethod = releaseMethod;
+    }
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(DicTraverseWrapper);
+    static void *(*sDicTraverseSessionFactoryMethod)(JNIEnv *, jstring);
+    static void (*sDicTraverseSessionInitMethod)(
+            void *, const Dictionary *const, const int *, const int);
+    static void (*sDicTraverseSessionReleaseMethod)(void *);
+};
+int register_DicTraverseSession(JNIEnv *env);
+} // namespace latinime
+#endif // LATINIME_DIC_TRAVERSE_WRAPPER_H
diff --git a/native/jni/src/dictionary.cpp b/native/jni/src/dictionary.cpp
index ee55cfa..2fbe83e 100644
--- a/native/jni/src/dictionary.cpp
+++ b/native/jni/src/dictionary.cpp
@@ -22,6 +22,7 @@
 #include "binary_format.h"
 #include "defines.h"
 #include "dictionary.h"
+#include "dic_traverse_wrapper.h"
 #include "gesture_decoder_wrapper.h"
 #include "unigram_dictionary.h"
 
@@ -29,10 +30,15 @@
 
 // TODO: Change the type of all keyCodes to uint32_t
 Dictionary::Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust,
-        int typedLetterMultiplier, int fullWordMultiplier,
-        int maxWordLength, int maxWords, int maxPredictions)
-    : mDict((unsigned char*) dict), mDictSize(dictSize),
-      mMmapFd(mmapFd), mDictBufAdjust(dictBufAdjust) {
+        int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords,
+        int maxPredictions)
+        : mDict(static_cast<unsigned char *>(dict)),
+          mOffsetDict((static_cast<unsigned char *>(dict)) + BinaryFormat::getHeaderSize(mDict)),
+          mDictSize(dictSize), mMmapFd(mmapFd), mDictBufAdjust(dictBufAdjust),
+          mUnigramDictionary(new UnigramDictionary(mOffsetDict, typedLetterMultiplier,
+                  fullWordMultiplier, maxWordLength, maxWords, BinaryFormat::getFlags(mDict))),
+          mBigramDictionary(new BigramDictionary(mOffsetDict, maxWordLength, maxPredictions)),
+          mGestureDecoder(new GestureDecoderWrapper(maxWordLength, maxWords)) {
     if (DEBUG_DICT) {
         if (MAX_WORD_LENGTH_INTERNAL < maxWordLength) {
             AKLOGI("Max word length (%d) is greater than %d",
@@ -40,14 +46,6 @@
             AKLOGI("IN NATIVE SUGGEST Version: %d", (mDict[0] & 0xFF));
         }
     }
-    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, maxPredictions);
-    mGestureDecoder = new GestureDecoderWrapper(maxWordLength, maxWords);
-    mGestureDecoder->setDict(mUnigramDictionary, mBigramDictionary,
-            mDict + headerSize /* dict root */, 0 /* root pos */);
 }
 
 Dictionary::~Dictionary() {
@@ -56,16 +54,18 @@
     delete mGestureDecoder;
 }
 
-int Dictionary::getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates, int *ycoordinates,
-        int *times, int *pointerIds, int *codes, int codesSize, int *prevWordChars,
+int Dictionary::getSuggestions(ProximityInfo *proximityInfo, void *traverseSession,
+        int *xcoordinates, int *ycoordinates, int *times, int *pointerIds,
+        int *codes, int codesSize, int *prevWordChars,
         int prevWordLength, int commitPoint, bool isGesture,
         bool useFullEditDistance, unsigned short *outWords,
-        int *frequencies, int *spaceIndices, int *outputTypes) {
+        int *frequencies, int *spaceIndices, int *outputTypes) const {
     int result = 0;
     if (isGesture) {
-        mGestureDecoder->setPrevWord(prevWordChars, prevWordLength);
-        result = mGestureDecoder->getSuggestions(proximityInfo, xcoordinates, ycoordinates,
-                times, pointerIds, codes, codesSize, commitPoint,
+        DicTraverseWrapper::initDicTraverseSession(
+                traverseSession, this, prevWordChars, prevWordLength);
+        result = mGestureDecoder->getSuggestions(proximityInfo, traverseSession,
+                xcoordinates, ycoordinates, times, pointerIds, codes, codesSize, commitPoint,
                 outWords, frequencies, spaceIndices, outputTypes);
         if (DEBUG_DICT) {
             DUMP_RESULT(outWords, frequencies, 18 /* MAX_WORDS */, MAX_WORD_LENGTH_INTERNAL);
diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h
index ab238c8..e9a03ce 100644
--- a/native/jni/src/dictionary.h
+++ b/native/jni/src/dictionary.h
@@ -44,18 +44,23 @@
     Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust, int typedLetterMultipler,
             int fullWordMultiplier, int maxWordLength, int maxWords, int maxPredictions);
 
-    int getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates, int *ycoordinates,
-            int *times, int *pointerIds, int *codes, int codesSize, int *prevWordChars,
-            int prevWordLength, int commitPoint, bool isGesture,
+    int getSuggestions(ProximityInfo *proximityInfo, void *traverseSession, int *xcoordinates,
+            int *ycoordinates, int *times, int *pointerIds, int *codes, int codesSize,
+            int *prevWordChars, int prevWordLength, int commitPoint, bool isGesture,
             bool useFullEditDistance, unsigned short *outWords,
-            int *frequencies, int *spaceIndices, int *outputTypes);
+            int *frequencies, int *spaceIndices, int *outputTypes) const;
 
     int getBigrams(const int32_t *word, int length, int *codes, int codesSize,
             unsigned short *outWords, int *frequencies, int *outputTypes) const;
 
     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; }
+    const uint8_t *getDict() const { // required to release dictionary buffer
+        return mDict;
+    }
+    const uint8_t *getOffsetDict() const {
+        return mOffsetDict;
+    }
     int getDictSize() const { return mDictSize; }
     int getMmapFd() const { return mMmapFd; }
     int getDictBufAdjust() const { return mDictBufAdjust; }
@@ -67,7 +72,8 @@
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Dictionary);
-    const unsigned char *mDict;
+    const uint8_t *mDict;
+    const uint8_t *mOffsetDict;
 
     // Used only for the mmap version of dictionary loading, but we use these as dummy variables
     // also for the malloc version.
@@ -85,8 +91,9 @@
 inline int Dictionary::wideStrLen(unsigned short *str) {
     if (!str) return 0;
     unsigned short *end = str;
-    while (*end)
+    while (*end) {
         end++;
+    }
     return end - str;
 }
 } // namespace latinime
diff --git a/native/jni/src/geometry_utils.h b/native/jni/src/geometry_utils.h
new file mode 100644
index 0000000..f30e9fc
--- /dev/null
+++ b/native/jni/src/geometry_utils.h
@@ -0,0 +1,97 @@
+/*
+ * 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_GEOMETRY_UTILS_H
+#define LATINIME_GEOMETRY_UTILS_H
+
+#include <cmath>
+
+#define MAX_PATHS 2
+
+#define DEBUG_DECODER false
+
+#define M_PI_F 3.14159265f
+
+namespace latinime {
+
+static inline float squareFloat(float x) {
+    return x * x;
+}
+
+static inline float getSquaredDistanceFloat(float x1, float y1, float x2, float y2) {
+    return squareFloat(x1 - x2) + squareFloat(y1 - y2);
+}
+
+static inline float getDistanceFloat(float x1, float y1, float x2, float y2) {
+    return hypotf(x1 - x2, y1 - y2);
+}
+
+static inline int getDistanceInt(int x1, int y1, int x2, int y2) {
+    return static_cast<int>(getDistanceFloat(static_cast<float>(x1), static_cast<float>(y1),
+            static_cast<float>(x2), static_cast<float>(y2)));
+}
+
+static inline float getAngle(int x1, int y1, int x2, int y2) {
+    const int dx = x1 - x2;
+    const int dy = y1 - y2;
+    if (dx == 0 && dy == 0) return 0;
+    return atan2f(static_cast<float>(dy), static_cast<float>(dx));
+}
+
+static inline float getAngleDiff(float a1, float a2) {
+    const float diff = fabsf(a1 - a2);
+    if (diff > M_PI_F) {
+        return 2.0f * M_PI_F - diff;
+    }
+    return diff;
+}
+
+// static float pointToLineSegSquaredDistanceFloat(
+//         float x, float y, float x1, float y1, float x2, float y2) {
+//     float A = x - x1;
+//     float B = y - y1;
+//     float C = x2 - x1;
+//     float D = y2 - y1;
+//     return fabsf(A * D - C * B) / sqrtf(C * C + D * D);
+// }
+
+static inline float pointToLineSegSquaredDistanceFloat(
+        float x, float y, float x1, float y1, float x2, float y2) {
+    const float ray1x = x - x1;
+    const float ray1y = y - y1;
+    const float ray2x = x2 - x1;
+    const float ray2y = y2 - y1;
+
+    const float dotProduct = ray1x * ray2x + ray1y * ray2y;
+    const float lineLengthSqr = squareFloat(ray2x) + squareFloat(ray2y);
+    const float projectionLengthSqr = dotProduct / lineLengthSqr;
+
+    float projectionX;
+    float projectionY;
+    if (projectionLengthSqr < 0.0f) {
+        projectionX = x1;
+        projectionY = y1;
+    } else if (projectionLengthSqr > 1.0f) {
+        projectionX = x2;
+        projectionY = y2;
+    } else {
+        projectionX = x1 + projectionLengthSqr * ray2x;
+        projectionY = y1 + projectionLengthSqr * ray2y;
+    }
+    return getSquaredDistanceFloat(x, y, projectionX, projectionY);
+}
+} // namespace latinime
+#endif // LATINIME_GEOMETRY_UTILS_H
diff --git a/native/jni/src/gesture/gesture_decoder_wrapper.h b/native/jni/src/gesture/gesture_decoder_wrapper.h
index 03c84b5..92e1ded 100644
--- a/native/jni/src/gesture/gesture_decoder_wrapper.h
+++ b/native/jni/src/gesture/gesture_decoder_wrapper.h
@@ -29,45 +29,24 @@
 
 class GestureDecoderWrapper : public IncrementalDecoderInterface {
  public:
-    GestureDecoderWrapper(const int maxWordLength, const int maxWords) {
-        mIncrementalDecoderInterface = getGestureDecoderInstance(maxWordLength, maxWords);
+    GestureDecoderWrapper(const int maxWordLength, const int maxWords)
+            : mIncrementalDecoderInterface(getGestureDecoderInstance(maxWordLength, maxWords)) {
     }
 
     virtual ~GestureDecoderWrapper() {
         delete mIncrementalDecoderInterface;
     }
 
-    int getSuggestions(ProximityInfo *pInfo, int *inputXs, int *inputYs, int *times,
-            int *pointerIds, int *codes, int inputSize, int commitPoint,
-            unsigned short *outWords, int *frequencies, int *outputIndices, int *outputTypes) {
+    int getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs, int *inputYs,
+            int *times, int *pointerIds, int *codes, int inputSize, int commitPoint,
+            unsigned short *outWords, int *frequencies, int *outputIndices,
+            int *outputTypes) const {
         if (!mIncrementalDecoderInterface) {
             return 0;
         }
         return mIncrementalDecoderInterface->getSuggestions(
-                pInfo, inputXs, inputYs, times, pointerIds, codes, inputSize, commitPoint,
-                outWords, frequencies, outputIndices, outputTypes);
-    }
-
-    void reset() {
-        if (!mIncrementalDecoderInterface) {
-            return;
-        }
-        mIncrementalDecoderInterface->reset();
-    }
-
-    void setDict(const UnigramDictionary *dict, const BigramDictionary *bigram,
-            const uint8_t *dictRoot, int rootPos) {
-        if (!mIncrementalDecoderInterface) {
-            return;
-        }
-        mIncrementalDecoderInterface->setDict(dict, bigram, dictRoot, rootPos);
-    }
-
-    void setPrevWord(const int32_t *prevWord, int prevWordLength) {
-        if (!mIncrementalDecoderInterface) {
-            return;
-        }
-        mIncrementalDecoderInterface->setPrevWord(prevWord, prevWordLength);
+                pInfo, traverseSession, inputXs, inputYs, times, pointerIds, codes,
+                inputSize, commitPoint, outWords, frequencies, outputIndices, outputTypes);
     }
 
     static void setGestureDecoderFactoryMethod(
@@ -76,7 +55,7 @@
     }
 
  private:
-    DISALLOW_COPY_AND_ASSIGN(GestureDecoderWrapper);
+    DISALLOW_IMPLICIT_CONSTRUCTORS(GestureDecoderWrapper);
     static IncrementalDecoderInterface *getGestureDecoderInstance(int maxWordLength, int maxWords) {
         if (sGestureDecoderFactoryMethod) {
             return sGestureDecoderFactoryMethod(maxWordLength, maxWords);
diff --git a/native/jni/src/gesture/incremental_decoder_interface.h b/native/jni/src/gesture/incremental_decoder_interface.h
index 6d2e273..d1395aa 100644
--- a/native/jni/src/gesture/incremental_decoder_interface.h
+++ b/native/jni/src/gesture/incremental_decoder_interface.h
@@ -28,14 +28,14 @@
 
 class IncrementalDecoderInterface {
  public:
-    virtual int getSuggestions(ProximityInfo *pInfo, int *inputXs, int *inputYs, int *times,
-            int *pointerIds, int *codes, int inputSize, int commitPoint,
-            unsigned short *outWords, int *frequencies, int *outputIndices, int *outputTypes) = 0;
-    virtual void reset() = 0;
-    virtual void setDict(const UnigramDictionary *dict, const BigramDictionary *bigram,
-            const uint8_t *dictRoot, int rootPos) = 0;
-    virtual void setPrevWord(const int32_t *prevWord, int prevWordLength) = 0;
+    virtual int getSuggestions(ProximityInfo *pInfo, void *traverseSession,
+            int *inputXs, int *inputYs, int *times, int *pointerIds, int *codes,
+            int inputSize, int commitPoint, unsigned short *outWords, int *frequencies,
+            int *outputIndices, int *outputTypes) const = 0;
+    IncrementalDecoderInterface() { };
     virtual ~IncrementalDecoderInterface() { };
+ private:
+    DISALLOW_COPY_AND_ASSIGN(IncrementalDecoderInterface);
 };
 } // namespace latinime
 #endif // LATINIME_INCREMENTAL_DECODER_INTERFACE_H
diff --git a/native/jni/src/gesture/incremental_decoder_wrapper.h b/native/jni/src/gesture/incremental_decoder_wrapper.h
index 6980615..da7afdb 100644
--- a/native/jni/src/gesture/incremental_decoder_wrapper.h
+++ b/native/jni/src/gesture/incremental_decoder_wrapper.h
@@ -29,45 +29,24 @@
 
 class IncrementalDecoderWrapper : public IncrementalDecoderInterface {
  public:
-    IncrementalDecoderWrapper(const int maxWordLength, const int maxWords) {
-        mIncrementalDecoderInterface = getIncrementalDecoderInstance(maxWordLength, maxWords);
+    IncrementalDecoderWrapper(const int maxWordLength, const int maxWords)
+            : mIncrementalDecoderInterface(getIncrementalDecoderInstance(maxWordLength, maxWords)) {
     }
 
     virtual ~IncrementalDecoderWrapper() {
         delete mIncrementalDecoderInterface;
     }
 
-    int getSuggestions(ProximityInfo *pInfo, int *inputXs, int *inputYs, int *times,
-            int *pointerIds, int *codes, int inputSize, int commitPoint,
-            unsigned short *outWords, int *frequencies, int *outputIndices, int *outputTypes) {
+    int getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs, int *inputYs,
+            int *times, int *pointerIds, int *codes, int inputSize, int commitPoint,
+            unsigned short *outWords, int *frequencies, int *outputIndices,
+            int *outputTypes) const {
         if (!mIncrementalDecoderInterface) {
             return 0;
         }
         return mIncrementalDecoderInterface->getSuggestions(
-                pInfo, inputXs, inputYs, times, pointerIds, codes, inputSize, commitPoint,
-                outWords, frequencies, outputIndices, outputTypes);
-    }
-
-    void reset() {
-        if (!mIncrementalDecoderInterface) {
-            return;
-        }
-        mIncrementalDecoderInterface->reset();
-    }
-
-    void setDict(const UnigramDictionary *dict, const BigramDictionary *bigram,
-            const uint8_t *dictRoot, int rootPos) {
-        if (!mIncrementalDecoderInterface) {
-            return;
-        }
-        mIncrementalDecoderInterface->setDict(dict, bigram, dictRoot, rootPos);
-    }
-
-    void setPrevWord(const int32_t *prevWord, int prevWordLength) {
-        if (!mIncrementalDecoderInterface) {
-            return;
-        }
-        mIncrementalDecoderInterface->setPrevWord(prevWord, prevWordLength);
+                pInfo, traverseSession, inputXs, inputYs, times, pointerIds, codes,
+                inputSize, commitPoint, outWords, frequencies, outputIndices, outputTypes);
     }
 
     static void setIncrementalDecoderFactoryMethod(
@@ -76,7 +55,7 @@
     }
 
  private:
-    DISALLOW_COPY_AND_ASSIGN(IncrementalDecoderWrapper);
+    DISALLOW_IMPLICIT_CONSTRUCTORS(IncrementalDecoderWrapper);
     static IncrementalDecoderInterface *getIncrementalDecoderInstance(int maxWordLength,
             int maxWords) {
         if (sIncrementalDecoderFactoryMethod) {
diff --git a/native/jni/src/hash_map_compat.h b/native/jni/src/hash_map_compat.h
new file mode 100644
index 0000000..116359a
--- /dev/null
+++ b/native/jni/src/hash_map_compat.h
@@ -0,0 +1,34 @@
+/*
+ * 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_HASH_MAP_COMPAT_H
+#define LATINIME_HASH_MAP_COMPAT_H
+
+// TODO: Use std::unordered_map that has been standardized in C++11
+
+#ifdef __APPLE__
+#include <ext/hash_map>
+#else // __APPLE__
+#include <hash_map>
+#endif // __APPLE__
+
+#ifdef __SGI_STL_PORT
+#define hash_map_compat stlport::hash_map
+#else // __SGI_STL_PORT
+#define hash_map_compat __gnu_cxx::hash_map
+#endif // __SGI_STL_PORT
+
+#endif // LATINIME_HASH_MAP_COMPAT_H
diff --git a/native/jni/src/proximity_info.cpp b/native/jni/src/proximity_info.cpp
index cee408d..e681f6f 100644
--- a/native/jni/src/proximity_info.cpp
+++ b/native/jni/src/proximity_info.cpp
@@ -17,34 +17,48 @@
 #include <cassert>
 #include <cmath>
 #include <cstring>
-#include <string>
 
 #define LOG_TAG "LatinIME: proximity_info.cpp"
 
 #include "additional_proximity_chars.h"
 #include "char_utils.h"
 #include "defines.h"
+#include "geometry_utils.h"
+#include "jni.h"
 #include "proximity_info.h"
 
 namespace latinime {
 
-inline void copyOrFillZero(void *to, const void *from, size_t size) {
-    if (from) {
-        memcpy(to, from, size);
-    } else {
-        memset(to, 0, size);
+/* static */ const int ProximityInfo::NOT_A_CODE = -1;
+/* static */ const float ProximityInfo::NOT_A_DISTANCE_FLOAT = -1.0f;
+
+static inline void safeGetOrFillZeroIntArrayRegion(JNIEnv *env, jintArray jArray, jsize len,
+        jint *buffer) {
+    if (jArray && buffer) {
+        env->GetIntArrayRegion(jArray, 0, len, buffer);
+    } else if (buffer) {
+        memset(buffer, 0, len * sizeof(jint));
     }
 }
 
-ProximityInfo::ProximityInfo(const std::string localeStr, const int maxProximityCharsSize,
+static inline void safeGetOrFillZeroFloatArrayRegion(JNIEnv *env, jfloatArray jArray, jsize len,
+        jfloat *buffer) {
+    if (jArray && buffer) {
+        env->GetFloatArrayRegion(jArray, 0, len, buffer);
+    } else if (buffer) {
+        memset(buffer, 0, len * sizeof(jfloat));
+    }
+}
+
+ProximityInfo::ProximityInfo(JNIEnv *env, const jstring localeJStr, const int maxProximityCharsSize,
         const int keyboardWidth, const int keyboardHeight, const int gridWidth,
-        const int gridHeight, const int mostCommonKeyWidth,
-        const int32_t *proximityCharsArray, const int keyCount, const int32_t *keyXCoordinates,
-        const int32_t *keyYCoordinates, const int32_t *keyWidths, const int32_t *keyHeights,
-        const int32_t *keyCharCodes, const float *sweetSpotCenterXs, const float *sweetSpotCenterYs,
-        const float *sweetSpotRadii)
-        : MAX_PROXIMITY_CHARS_SIZE(maxProximityCharsSize), KEYBOARD_WIDTH(keyboardWidth),
-          KEYBOARD_HEIGHT(keyboardHeight), GRID_WIDTH(gridWidth), GRID_HEIGHT(gridHeight),
+        const int gridHeight, const int mostCommonKeyWidth, const jintArray proximityChars,
+        const int keyCount, const jintArray keyXCoordinates, const jintArray keyYCoordinates,
+        const jintArray keyWidths, const jintArray keyHeights, const jintArray keyCharCodes,
+        const jfloatArray sweetSpotCenterXs, const jfloatArray sweetSpotCenterYs,
+        const jfloatArray sweetSpotRadii)
+        : MAX_PROXIMITY_CHARS_SIZE(maxProximityCharsSize), GRID_WIDTH(gridWidth),
+          GRID_HEIGHT(gridHeight), MOST_COMMON_KEY_WIDTH(mostCommonKeyWidth),
           MOST_COMMON_KEY_WIDTH_SQUARE(mostCommonKeyWidth * mostCommonKeyWidth),
           CELL_WIDTH((keyboardWidth + gridWidth - 1) / gridWidth),
           CELL_HEIGHT((keyboardHeight + gridHeight - 1) / gridHeight),
@@ -52,27 +66,30 @@
           HAS_TOUCH_POSITION_CORRECTION_DATA(keyCount > 0 && keyXCoordinates && keyYCoordinates
                   && keyWidths && keyHeights && keyCharCodes && sweetSpotCenterXs
                   && sweetSpotCenterYs && sweetSpotRadii),
-          mLocaleStr(localeStr) {
+          mProximityCharsArray(new int32_t[GRID_WIDTH * GRID_HEIGHT * MAX_PROXIMITY_CHARS_SIZE
+                  /* proximityGridLength */]) {
     const int proximityGridLength = GRID_WIDTH * GRID_HEIGHT * MAX_PROXIMITY_CHARS_SIZE;
     if (DEBUG_PROXIMITY_INFO) {
         AKLOGI("Create proximity info array %d", proximityGridLength);
     }
-    mProximityCharsArray = new int32_t[proximityGridLength];
-    memcpy(mProximityCharsArray, proximityCharsArray,
-            proximityGridLength * sizeof(mProximityCharsArray[0]));
-
-    copyOrFillZero(mKeyXCoordinates, keyXCoordinates, KEY_COUNT * sizeof(mKeyXCoordinates[0]));
-    copyOrFillZero(mKeyYCoordinates, keyYCoordinates, KEY_COUNT * sizeof(mKeyYCoordinates[0]));
-    copyOrFillZero(mKeyWidths, keyWidths, KEY_COUNT * sizeof(mKeyWidths[0]));
-    copyOrFillZero(mKeyHeights, keyHeights, KEY_COUNT * sizeof(mKeyHeights[0]));
-    copyOrFillZero(mKeyCharCodes, keyCharCodes, KEY_COUNT * sizeof(mKeyCharCodes[0]));
-    copyOrFillZero(mSweetSpotCenterXs, sweetSpotCenterXs,
-            KEY_COUNT * sizeof(mSweetSpotCenterXs[0]));
-    copyOrFillZero(mSweetSpotCenterYs, sweetSpotCenterYs,
-            KEY_COUNT * sizeof(mSweetSpotCenterYs[0]));
-    copyOrFillZero(mSweetSpotRadii, sweetSpotRadii, KEY_COUNT * sizeof(mSweetSpotRadii[0]));
-
+    const jsize localeCStrUtf8Length = env->GetStringUTFLength(localeJStr);
+    if (localeCStrUtf8Length >= MAX_LOCALE_STRING_LENGTH) {
+        AKLOGI("Locale string length too long: length=%d", localeCStrUtf8Length);
+        assert(false);
+    }
+    memset(mLocaleStr, 0, sizeof(mLocaleStr));
+    env->GetStringUTFRegion(localeJStr, 0, env->GetStringLength(localeJStr), mLocaleStr);
+    safeGetOrFillZeroIntArrayRegion(env, proximityChars, proximityGridLength, mProximityCharsArray);
+    safeGetOrFillZeroIntArrayRegion(env, keyXCoordinates, KEY_COUNT, mKeyXCoordinates);
+    safeGetOrFillZeroIntArrayRegion(env, keyYCoordinates, KEY_COUNT, mKeyYCoordinates);
+    safeGetOrFillZeroIntArrayRegion(env, keyWidths, KEY_COUNT, mKeyWidths);
+    safeGetOrFillZeroIntArrayRegion(env, keyHeights, KEY_COUNT, mKeyHeights);
+    safeGetOrFillZeroIntArrayRegion(env, keyCharCodes, KEY_COUNT, mKeyCharCodes);
+    safeGetOrFillZeroFloatArrayRegion(env, sweetSpotCenterXs, KEY_COUNT, mSweetSpotCenterXs);
+    safeGetOrFillZeroFloatArrayRegion(env, sweetSpotCenterYs, KEY_COUNT, mSweetSpotCenterYs);
+    safeGetOrFillZeroFloatArrayRegion(env, sweetSpotRadii, KEY_COUNT, mSweetSpotRadii);
     initializeCodeToKeyIndex();
+    initializeG();
 }
 
 // Build the reversed look up table from the char code to the index in mKeyXCoordinates,
@@ -121,6 +138,21 @@
     return false;
 }
 
+static inline float getNormalizedSquaredDistanceFloat(float x1, float y1, float x2, float y2,
+        float scale) {
+    return squareFloat((x1 - x2) / scale) + squareFloat((y1 - y2) / scale);
+}
+
+float ProximityInfo::getNormalizedSquaredDistanceFromCenterFloat(
+        const int keyId, const int x, const int y) const {
+    const float centerX = static_cast<float>(getKeyCenterXOfIdG(keyId));
+    const float centerY = static_cast<float>(getKeyCenterYOfIdG(keyId));
+    const float touchX = static_cast<float>(x);
+    const float touchY = static_cast<float>(y);
+    const float keyWidth = static_cast<float>(getMostCommonKeyWidth());
+    return getNormalizedSquaredDistanceFloat(centerX, centerY, touchX, touchY, keyWidth);
+}
+
 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];
@@ -160,7 +192,7 @@
             }
         }
         const int additionalProximitySize =
-                AdditionalProximityChars::getAdditionalCharsSize(&mLocaleStr, primaryKey);
+                AdditionalProximityChars::getAdditionalCharsSize(mLocaleStr, primaryKey);
         if (additionalProximitySize > 0) {
             inputCodes[insertPos++] = ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE;
             if (insertPos >= MAX_PROXIMITY_CHARS_SIZE) {
@@ -171,7 +203,7 @@
             }
 
             const int32_t *additionalProximityChars =
-                    AdditionalProximityChars::getAdditionalChars(&mLocaleStr, primaryKey);
+                    AdditionalProximityChars::getAdditionalChars(mLocaleStr, primaryKey);
             for (int j = 0; j < additionalProximitySize; ++j) {
                 const int32_t ac = additionalProximityChars[j];
                 int k = 0;
@@ -211,24 +243,65 @@
     return mCodeToKeyIndex[baseLowerC];
 }
 
-// TODO: [Staging] Optimize
-void ProximityInfo::getCenters(int *centerXs, int *centerYs, int *codeToKeyIndex,
-        int *keyToCodeIndex, int *keyCount, int *keyWidth) const {
-    *keyCount = KEY_COUNT;
-    *keyWidth = sqrt(static_cast<float>(MOST_COMMON_KEY_WIDTH_SQUARE));
+int ProximityInfo::getKeyCode(const int keyIndex) const {
+    if (keyIndex < 0 || keyIndex >= KEY_COUNT) {
+        return NOT_AN_INDEX;
+    }
+    return mKeyToCodeIndexG[keyIndex];
+}
 
+void ProximityInfo::initializeG() {
+    // TODO: Optimize
     for (int i = 0; i < KEY_COUNT; ++i) {
         const int code = mKeyCharCodes[i];
         const int lowerCode = toBaseLowerCase(code);
-        centerXs[i] = mKeyXCoordinates[i] + mKeyWidths[i] / 2;
-        centerYs[i] = mKeyYCoordinates[i] + mKeyHeights[i] / 2;
-        codeToKeyIndex[code] = i;
+        mCenterXsG[i] = mKeyXCoordinates[i] + mKeyWidths[i] / 2;
+        mCenterYsG[i] = mKeyYCoordinates[i] + mKeyHeights[i] / 2;
         if (code != lowerCode && lowerCode >= 0 && lowerCode <= MAX_CHAR_CODE) {
-            codeToKeyIndex[lowerCode] = i;
-            keyToCodeIndex[i] = lowerCode;
+            mCodeToKeyIndex[lowerCode] = i;
+            mKeyToCodeIndexG[i] = lowerCode;
         } else {
-            keyToCodeIndex[i] = code;
+            mKeyToCodeIndexG[i] = code;
         }
     }
+    for (int i = 0; i < KEY_COUNT; i++) {
+        mKeyKeyDistancesG[i][i] = 0;
+        for (int j = i + 1; j < KEY_COUNT; j++) {
+            mKeyKeyDistancesG[i][j] = getDistanceInt(
+                    mCenterXsG[i], mCenterYsG[i], mCenterXsG[j], mCenterYsG[j]);
+            mKeyKeyDistancesG[j][i] = mKeyKeyDistancesG[i][j];
+        }
+    }
+}
+
+float ProximityInfo::getKeyCenterXOfCharG(int charCode) const {
+    return getKeyCenterXOfIdG(getKeyIndex(charCode));
+}
+
+float ProximityInfo::getKeyCenterYOfCharG(int charCode) const {
+    return getKeyCenterYOfIdG(getKeyIndex(charCode));
+}
+
+float ProximityInfo::getKeyCenterXOfIdG(int keyId) const {
+    if (keyId >= 0) {
+        return mCenterXsG[keyId];
+    }
+    return 0;
+}
+
+float ProximityInfo::getKeyCenterYOfIdG(int keyId) const {
+    if (keyId >= 0) {
+        return mCenterYsG[keyId];
+    }
+    return 0;
+}
+
+int ProximityInfo::getKeyKeyDistanceG(int key0, int key1) const {
+    const int keyId0 = getKeyIndex(key0);
+    const int keyId1 = getKeyIndex(key1);
+    if (keyId0 >= 0 && keyId1 >= 0) {
+        return mKeyKeyDistancesG[keyId0][keyId1];
+    }
+    return 0;
 }
 } // namespace latinime
diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h
index abd07dd..822909b 100644
--- a/native/jni/src/proximity_info.h
+++ b/native/jni/src/proximity_info.h
@@ -18,9 +18,9 @@
 #define LATINIME_PROXIMITY_INFO_H
 
 #include <stdint.h>
-#include <string>
 
 #include "defines.h"
+#include "jni.h"
 
 namespace latinime {
 
@@ -28,31 +28,25 @@
 
 class ProximityInfo {
  public:
-    ProximityInfo(const std::string localeStr, const int maxProximityCharsSize,
+    ProximityInfo(JNIEnv *env, const jstring localeJStr, const int maxProximityCharsSize,
             const int keyboardWidth, const int keyboardHeight, const int gridWidth,
-            const int gridHeight, const int mostCommonkeyWidth,
-            const int32_t *proximityCharsArray, const int keyCount, const int32_t *keyXCoordinates,
-            const int32_t *keyYCoordinates, const int32_t *keyWidths, const int32_t *keyHeights,
-            const int32_t *keyCharCodes, const float *sweetSpotCenterXs,
-            const float *sweetSpotCenterYs, const float *sweetSpotRadii);
+            const int gridHeight, const int mostCommonKeyWidth, const jintArray proximityChars,
+            const int keyCount, const jintArray keyXCoordinates, const jintArray keyYCoordinates,
+            const jintArray keyWidths, const jintArray keyHeights, const jintArray keyCharCodes,
+            const jfloatArray sweetSpotCenterXs, const jfloatArray sweetSpotCenterYs,
+            const jfloatArray sweetSpotRadii);
     ~ProximityInfo();
     bool hasSpaceProximity(const int x, const int y) const;
     int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const;
+    float getNormalizedSquaredDistanceFromCenterFloat(
+            const int keyId, const int x, const int y) const;
     bool sameAsTyped(const unsigned short *word, int length) const;
-    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;
-    }
     int getKeyIndex(const int c) const;
+    int getKeyCode(const int keyIndex) 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;
+        return mSweetSpotRadii[keyIndex] > 0.0f;
     }
     float getSweetSpotRadiiAt(int keyIndex) const {
         return mSweetSpotRadii[keyIndex];
@@ -70,11 +64,15 @@
         return HAS_TOUCH_POSITION_CORRECTION_DATA;
     }
 
+    int getMostCommonKeyWidth() const {
+        return MOST_COMMON_KEY_WIDTH;
+    }
+
     int getMostCommonKeyWidthSquare() const {
         return MOST_COMMON_KEY_WIDTH_SQUARE;
     }
 
-    std::string getLocaleStr() const {
+    const char *getLocaleStr() const {
         return mLocaleStr;
     }
 
@@ -98,9 +96,11 @@
         return GRID_HEIGHT;
     }
 
-    // Returns the keyboard key-center information.
-    void getCenters(int *centersX, int *centersY, int *codeToKeyIndex, int *keyToCodeIndex,
-            int *keyCount, int *keyWidth) const;
+    float getKeyCenterXOfCharG(int charCode) const;
+    float getKeyCenterYOfCharG(int charCode) const;
+    float getKeyCenterXOfIdG(int keyId) const;
+    float getKeyCenterYOfIdG(int keyId) const;
+    int getKeyKeyDistanceG(int key0, int key1) const;
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(ProximityInfo);
@@ -108,27 +108,36 @@
     static const int MAX_KEY_COUNT_IN_A_KEYBOARD = 64;
     // 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;
+    static const int NOT_A_CODE;
+    static const float NOT_A_DISTANCE_FLOAT;
 
     int getStartIndexFromCoordinates(const int x, const int y) const;
     void initializeCodeToKeyIndex();
+    void initializeG();
     float calculateNormalizedSquaredDistance(const int keyIndex, const int inputIndex) const;
     float calculateSquaredDistanceFromSweetSpotCenter(
             const int keyIndex, const int inputIndex) const;
     bool hasInputCoordinates() const;
+    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;
+    }
 
     const int MAX_PROXIMITY_CHARS_SIZE;
-    const int KEYBOARD_WIDTH;
-    const int KEYBOARD_HEIGHT;
     const int GRID_WIDTH;
     const int GRID_HEIGHT;
+    const int MOST_COMMON_KEY_WIDTH;
     const int MOST_COMMON_KEY_WIDTH_SQUARE;
     const int CELL_WIDTH;
     const int CELL_HEIGHT;
     const int KEY_COUNT;
     const bool HAS_TOUCH_POSITION_CORRECTION_DATA;
-    const std::string mLocaleStr;
+    char mLocaleStr[MAX_LOCALE_STRING_LENGTH];
     int32_t *mProximityCharsArray;
     int32_t mKeyXCoordinates[MAX_KEY_COUNT_IN_A_KEYBOARD];
     int32_t mKeyYCoordinates[MAX_KEY_COUNT_IN_A_KEYBOARD];
@@ -139,6 +148,11 @@
     float mSweetSpotCenterYs[MAX_KEY_COUNT_IN_A_KEYBOARD];
     float mSweetSpotRadii[MAX_KEY_COUNT_IN_A_KEYBOARD];
     int mCodeToKeyIndex[MAX_CHAR_CODE + 1];
+
+    int mKeyToCodeIndexG[MAX_KEY_COUNT_IN_A_KEYBOARD];
+    int mCenterXsG[MAX_KEY_COUNT_IN_A_KEYBOARD];
+    int mCenterYsG[MAX_KEY_COUNT_IN_A_KEYBOARD];
+    int mKeyKeyDistancesG[MAX_KEY_COUNT_IN_A_KEYBOARD][MAX_KEY_COUNT_IN_A_KEYBOARD];
     // 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
index 86c8a69..f01b81e 100644
--- a/native/jni/src/proximity_info_state.cpp
+++ b/native/jni/src/proximity_info_state.cpp
@@ -20,13 +20,15 @@
 #define LOG_TAG "LatinIME: proximity_info_state.cpp"
 
 #include "defines.h"
+#include "geometry_utils.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) {
+void ProximityInfoState::initInputParams(const int pointerId, const float maxPointToKeyLength,
+        const ProximityInfo *proximityInfo, const int32_t *const inputCodes, const int inputSize,
+        const int *const xCoordinates, const int *const yCoordinates, const int *const times,
+        const int *const pointerIds, const bool isGeometric) {
     mProximityInfo = proximityInfo;
     mHasTouchPositionCorrectionData = proximityInfo->hasTouchPositionCorrectionData();
     mMostCommonKeyWidthSquare = proximityInfo->getMostCommonKeyWidthSquare();
@@ -36,76 +38,146 @@
     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]));
+    memset(mInputCodes, 0, sizeof(mInputCodes));
 
-    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);
-            }
+    if (!isGeometric && pointerId == 0) {
+        // Initialize
+        // - mInputCodes
+        // - mNormalizedSquaredDistances
+        // TODO: Merge
+        for (int i = 0; i < inputSize; ++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);
         }
-    }
-    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);
+            for (int i = 0; i < inputSize; ++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);
+                }
             }
         }
     }
+
+    ///////////////////////
+    // Setup touch points
+    mMaxPointToKeyLength = maxPointToKeyLength;
+    mInputXs.clear();
+    mInputYs.clear();
+    mTimes.clear();
+    mLengthCache.clear();
+    mDistanceCache.clear();
+
+    mInputSize = 0;
+    if (xCoordinates && yCoordinates) {
+        const bool proximityOnly = !isGeometric && (xCoordinates[0] < 0 || yCoordinates[0] < 0);
+        for (int i = 0; i < inputSize; ++i) {
+            // Assuming pointerId == 0 if pointerIds is null.
+            const int pid = pointerIds ? pointerIds[i] : 0;
+            if (pointerId == pid) {
+                const int c = isGeometric ? NOT_A_COORDINATE : getPrimaryCharAt(i);
+                const int x = proximityOnly ? NOT_A_COORDINATE : xCoordinates[i];
+                const int y = proximityOnly ? NOT_A_COORDINATE : yCoordinates[i];
+                const int time = times ? times[i] : -1;
+                if (pushTouchPoint(c, x, y, time, isGeometric)) {
+                    ++mInputSize;
+                }
+            }
+        }
+    }
+
+    if (mInputSize > 0) {
+        const int keyCount = mProximityInfo->getKeyCount();
+        mDistanceCache.resize(mInputSize * keyCount);
+        for (int i = 0; i < mInputSize; ++i) {
+            for (int k = 0; k < keyCount; ++k) {
+                const int index = i * keyCount + k;
+                const int x = mInputXs[i];
+                const int y = mInputYs[i];
+                mDistanceCache[index] =
+                        mProximityInfo->getNormalizedSquaredDistanceFromCenterFloat(k, x, y);
+            }
+        }
+    }
+
+    // end
+    ///////////////////////
+
+    memset(mNormalizedSquaredDistances, NOT_A_DISTANCE, sizeof(mNormalizedSquaredDistances));
+    memset(mPrimaryInputWord, 0, sizeof(mPrimaryInputWord));
+    mTouchPositionCorrectionEnabled = mInputSize > 0 && mHasTouchPositionCorrectionData
+            && xCoordinates && yCoordinates && !isGeometric;
+    if (!isGeometric && pointerId == 0) {
+        for (int i = 0; i < inputSize; ++i) {
+            mPrimaryInputWord[i] = getPrimaryCharAt(i);
+        }
+
+        for (int i = 0; i < mInputSize && mTouchPositionCorrectionEnabled; ++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);
+                }
+            }
+        }
+    }
+}
+
+bool ProximityInfoState::pushTouchPoint(const int nodeChar, int x, int y,
+        const int time, const bool sample) {
+    const uint32_t size = mInputXs.size();
+    // TODO: Should have a const variable for 10
+    const int sampleRate = mProximityInfo->getMostCommonKeyWidth() / 10;
+    if (size > 0) {
+        const int dist = getDistanceInt(x, y, mInputXs[size - 1], mInputYs[size - 1]);
+        if (sample && dist < sampleRate) {
+            return false;
+        }
+        mLengthCache.push_back(mLengthCache[size - 1] + dist);
+    } else {
+        mLengthCache.push_back(0);
+    }
+    if (nodeChar >= 0 && (x < 0 || y < 0)) {
+        const int keyId = mProximityInfo->getKeyIndex(nodeChar);
+        if (keyId >= 0) {
+            x = mProximityInfo->getKeyCenterXOfIdG(keyId);
+            y = mProximityInfo->getKeyCenterYOfIdG(keyId);
+        }
+    }
+    mInputXs.push_back(x);
+    mInputYs.push_back(y);
+    mTimes.push_back(time);
+    return true;
 }
 
 float ProximityInfoState::calculateNormalizedSquaredDistance(
@@ -116,7 +188,7 @@
     if (!mProximityInfo->hasSweetSpotData(keyIndex)) {
         return NOT_A_DISTANCE_FLOAT;
     }
-    if (NOT_A_COORDINATE == mInputXCoordinates[inputIndex]) {
+    if (NOT_A_COORDINATE == mInputXs[inputIndex]) {
         return NOT_A_DISTANCE_FLOAT;
     }
     const float squaredDistance = calculateSquaredDistanceFromSweetSpotCenter(
@@ -125,12 +197,37 @@
     return squaredDistance / squaredRadius;
 }
 
+int ProximityInfoState::getDuration(const int index) const {
+    if (mInputSize > 0 && index > 0 && index < static_cast<int>(mInputSize) - 1) {
+        return mTimes[index + 1] - mTimes[index - 1];
+    }
+    return 0;
+}
+
+float ProximityInfoState::getPointToKeyLength(int inputIndex, int charCode, float scale) {
+    const int keyId = mProximityInfo->getKeyIndex(charCode);
+    if (keyId >= 0) {
+        const int index = inputIndex * mProximityInfo->getKeyCount() + keyId;
+        return min(mDistanceCache[index] * scale, mMaxPointToKeyLength);
+    }
+    return 0;
+}
+
+int ProximityInfoState::getKeyKeyDistance(int key0, int key1) {
+    return mProximityInfo->getKeyKeyDistanceG(key0, key1);
+}
+
+int ProximityInfoState::getSpaceY() {
+    const int keyId = mProximityInfo->getKeyIndex(' ');
+    return mProximityInfo->getKeyCenterYOfIdG(keyId);
+}
+
 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 = static_cast<float>(mInputXCoordinates[inputIndex]);
-    const float inputY = static_cast<float>(mInputYCoordinates[inputIndex]);
+    const float inputX = static_cast<float>(mInputXs[inputIndex]);
+    const float inputY = static_cast<float>(mInputYs[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
index 76d4551..26fd89b 100644
--- a/native/jni/src/proximity_info_state.h
+++ b/native/jni/src/proximity_info_state.h
@@ -17,8 +17,10 @@
 #ifndef LATINIME_PROXIMITY_INFO_STATE_H
 #define LATINIME_PROXIMITY_INFO_STATE_H
 
+#include <cstring> // for memset()
 #include <stdint.h>
 #include <string>
+#include <vector>
 
 #include "char_utils.h"
 #include "defines.h"
@@ -40,18 +42,27 @@
     /////////////////////////////////////////
     // Defined in proximity_info_state.cpp //
     /////////////////////////////////////////
-    void initInputParams(
-            const ProximityInfo *proximityInfo, const int32_t *inputCodes, const int inputLength,
-            const int *xCoordinates, const int *yCoordinates);
+    void initInputParams(const int pointerId, const float maxPointToKeyLength,
+            const ProximityInfo *proximityInfo, const int32_t *const inputCodes,
+            const int inputSize, const int *xCoordinates, const int *yCoordinates,
+            const int *const times, const int *const pointerIds, const bool isGeometric);
 
     /////////////////////////////////////////
     // Defined here                        //
     /////////////////////////////////////////
-    ProximityInfoState() {};
-    inline const int *getProximityCharsAt(const int index) const {
-        return mInputCodes + (index * MAX_PROXIMITY_CHARS_SIZE_INTERNAL);
+    ProximityInfoState()
+            : mProximityInfo(0), mMaxPointToKeyLength(0),
+              mHasTouchPositionCorrectionData(false), mMostCommonKeyWidthSquare(0), mLocaleStr(),
+              mKeyCount(0), mCellHeight(0), mCellWidth(0), mGridHeight(0), mGridWidth(0),
+              mInputXs(), mInputYs(), mTimes(), mDistanceCache(), mLengthCache(),
+              mTouchPositionCorrectionEnabled(false), mInputSize(0) {
+        memset(mInputCodes, 0, sizeof(mInputCodes));
+        memset(mNormalizedSquaredDistances, 0, sizeof(mNormalizedSquaredDistances));
+        memset(mPrimaryInputWord, 0, sizeof(mPrimaryInputWord));
     }
 
+    virtual ~ProximityInfoState() {}
+
     inline unsigned short getPrimaryCharAt(const int index) const {
         return getProximityCharsAt(index)[0];
     }
@@ -68,14 +79,14 @@
     }
 
     inline bool existsAdjacentProximityChars(const int index) const {
-        if (index < 0 || index >= mInputLength) return false;
+        if (index < 0 || index >= mInputSize) 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)) {
+        if (rightIndex < mInputSize && existsCharInProximityAt(rightIndex, currentChar)) {
             return true;
         }
         return false;
@@ -160,6 +171,49 @@
         return mTouchPositionCorrectionEnabled;
     }
 
+    inline bool sameAsTyped(const unsigned short *word, int length) const {
+        if (length != mInputSize) {
+            return false;
+        }
+        const int *inputCodes = mInputCodes;
+        while (length--) {
+            if (static_cast<unsigned int>(*inputCodes) != static_cast<unsigned int>(*word)) {
+                return false;
+            }
+            inputCodes += MAX_PROXIMITY_CHARS_SIZE_INTERNAL;
+            word++;
+        }
+        return true;
+    }
+
+    int getDuration(const int index) const;
+
+    bool isUsed() const {
+        return mInputSize > 0;
+    }
+
+    uint32_t size() const {
+        return mInputSize;
+    }
+
+    int getInputX(int index) const {
+        return mInputXs[index];
+    }
+
+    int getInputY(int index) const {
+        return mInputYs[index];
+    }
+
+    int getLengthCache(int index) const {
+        return mLengthCache[index];
+    }
+
+    float getPointToKeyLength(int inputIndex, int charCode, float scale);
+
+    int getKeyKeyDistance(int key0, int key1);
+
+    int getSpaceY();
+
  private:
     DISALLOW_COPY_AND_ASSIGN(ProximityInfoState);
     /////////////////////////////////////////
@@ -170,32 +224,23 @@
     float calculateSquaredDistanceFromSweetSpotCenter(
             const int keyIndex, const int inputIndex) const;
 
+    bool pushTouchPoint(const int nodeChar, int x, int y, const int time, const bool sample);
     /////////////////////////////////////////
     // Defined here                        //
     /////////////////////////////////////////
     inline float square(const float x) const { return x * x; }
 
     bool hasInputCoordinates() const {
-        return mInputXCoordinates && mInputYCoordinates;
+        return mInputXs.size() > 0 && mInputYs.size() > 0;
     }
 
-    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;
+    inline const int *getProximityCharsAt(const int index) const {
+        return mInputCodes + (index * MAX_PROXIMITY_CHARS_SIZE_INTERNAL);
     }
 
     // const
     const ProximityInfo *mProximityInfo;
+    float mMaxPointToKeyLength;
     bool mHasTouchPositionCorrectionData;
     int mMostCommonKeyWidthSquare;
     std::string mLocaleStr;
@@ -205,12 +250,15 @@
     int mGridHeight;
     int mGridWidth;
 
-    const int *mInputXCoordinates;
-    const int *mInputYCoordinates;
+    std::vector<int> mInputXs;
+    std::vector<int> mInputYs;
+    std::vector<int> mTimes;
+    std::vector<float> mDistanceCache;
+    std::vector<int>  mLengthCache;
     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;
+    int mInputSize;
     unsigned short mPrimaryInputWord[MAX_WORD_LENGTH_INTERNAL];
 };
 } // namespace latinime
diff --git a/native/jni/src/terminal_attributes.h b/native/jni/src/terminal_attributes.h
index d633645..34ab8f0 100644
--- a/native/jni/src/terminal_attributes.h
+++ b/native/jni/src/terminal_attributes.h
@@ -30,13 +30,13 @@
  public:
     class ShortcutIterator {
         const uint8_t *const mDict;
-        bool mHasNextShortcutTarget;
         int mPos;
+        bool mHasNextShortcutTarget;
 
      public:
-        ShortcutIterator(const uint8_t *dict, const int pos, const uint8_t flags) : mDict(dict),
-                mPos(pos) {
-            mHasNextShortcutTarget = (0 != (flags & BinaryFormat::FLAG_HAS_SHORTCUT_TARGETS));
+        ShortcutIterator(const uint8_t *dict, const int pos, const uint8_t flags)
+                : mDict(dict), mPos(pos),
+                  mHasNextShortcutTarget(0 != (flags & BinaryFormat::FLAG_HAS_SHORTCUT_TARGETS)) {
         }
 
         inline bool hasNextShortcutTarget() const {
@@ -46,7 +46,7 @@
         // Gets the shortcut target itself as a uint16_t string. For parameters and return value
         // see BinaryFormat::getWordAtAddress.
         // TODO: make the output an uint32_t* to handle the whole unicode range.
-        inline int getNextShortcutTarget(const int maxDepth, uint16_t *outWord) {
+        inline int getNextShortcutTarget(const int maxDepth, uint16_t *outWord, int *outFreq) {
             const int shortcutFlags = BinaryFormat::getFlagsAndForwardPointer(mDict, &mPos);
             mHasNextShortcutTarget =
                     0 != (shortcutFlags & BinaryFormat::FLAG_ATTRIBUTE_HAS_NEXT);
@@ -56,18 +56,12 @@
                 if (NOT_A_CHARACTER == charCode) break;
                 outWord[i] = (uint16_t)charCode;
             }
+            *outFreq = BinaryFormat::getAttributeFrequencyFromFlags(shortcutFlags);
             mPos += BinaryFormat::CHARACTER_ARRAY_TERMINATOR_SIZE;
             return i;
         }
     };
 
- private:
-    DISALLOW_IMPLICIT_CONSTRUCTORS(TerminalAttributes);
-    const uint8_t *const mDict;
-    const uint8_t mFlags;
-    const int mStartPos;
-
- public:
     TerminalAttributes(const uint8_t *const dict, const uint8_t flags, const int pos) :
             mDict(dict), mFlags(flags), mStartPos(pos) {
     }
@@ -77,6 +71,12 @@
         // skipped quickly, so we ignore it.
         return ShortcutIterator(mDict, mStartPos + BinaryFormat::SHORTCUT_LIST_SIZE_SIZE, mFlags);
     }
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(TerminalAttributes);
+    const uint8_t *const mDict;
+    const uint8_t mFlags;
+    const int mStartPos;
 };
 } // namespace latinime
 #endif // LATINIME_TERMINAL_ATTRIBUTES_H
diff --git a/native/jni/src/unigram_dictionary.cpp b/native/jni/src/unigram_dictionary.cpp
index b6b0210..ba3c2db 100644
--- a/native/jni/src/unigram_dictionary.cpp
+++ b/native/jni/src/unigram_dictionary.cpp
@@ -63,8 +63,8 @@
 
 // TODO: This needs to take a const unsigned short* and not tinker with its contents
 static inline void addWord(
-        unsigned short *word, int length, int frequency, WordsPriorityQueue *queue) {
-    queue->push(frequency, word, length);
+        unsigned short *word, int length, int frequency, WordsPriorityQueue *queue, int type) {
+    queue->push(frequency, word, length, type);
 }
 
 // Return the replacement code point for a digraph, or 0 if none.
@@ -213,8 +213,8 @@
         AKLOGI("Max normalized score = %f", ns);
     }
     const int suggestedWordsCount =
-            queuePool.getMasterQueue()->outputSuggestions(
-                    masterCorrection.getPrimaryInputWord(), codesSize, frequencies, outWords);
+            queuePool.getMasterQueue()->outputSuggestions(masterCorrection.getPrimaryInputWord(),
+                    codesSize, frequencies, outWords, outputTypes);
 
     if (DEBUG_DICT) {
         float ns = queuePool.getMasterQueue()->getHighestNormalizedScore(
@@ -237,7 +237,7 @@
 
 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 int inputSize, const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
         const bool useFullEditDistance, Correction *correction,
         WordsPriorityQueuePool *queuePool) const {
 
@@ -247,7 +247,7 @@
 
     PROF_START(1);
     getOneWordSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, bigramMap, bigramFilter,
-            useFullEditDistance, inputLength, correction, queuePool);
+            useFullEditDistance, inputSize, correction, queuePool);
     PROF_END(1);
 
     PROF_START(2);
@@ -263,7 +263,7 @@
     WordsPriorityQueue *masterQueue = queuePool->getMasterQueue();
     if (masterQueue->size() > 0) {
         float nsForMaster = masterQueue->getHighestNormalizedScore(
-                correction->getPrimaryInputWord(), inputLength, 0, 0, 0);
+                correction->getPrimaryInputWord(), inputSize, 0, 0, 0);
         hasAutoCorrectionCandidate = (nsForMaster > START_TWO_WORDS_CORRECTION_THRESHOLD);
     }
     PROF_END(4);
@@ -271,9 +271,9 @@
     PROF_START(5);
     // Multiple word suggestions
     if (SUGGEST_MULTIPLE_WORDS
-            && inputLength >= MIN_USER_TYPED_LENGTH_FOR_MULTIPLE_WORD_SUGGESTION) {
+            && inputSize >= MIN_USER_TYPED_LENGTH_FOR_MULTIPLE_WORD_SUGGESTION) {
         getSplitMultipleWordsSuggestions(proximityInfo, xcoordinates, ycoordinates, codes,
-                useFullEditDistance, inputLength, correction, queuePool,
+                useFullEditDistance, inputSize, correction, queuePool,
                 hasAutoCorrectionCandidate);
     }
     PROF_END(5);
@@ -304,15 +304,15 @@
 }
 
 void UnigramDictionary::initSuggestions(ProximityInfo *proximityInfo, const int *xCoordinates,
-        const int *yCoordinates, const int *codes, const int inputLength,
+        const int *yCoordinates, const int *codes, const int inputSize,
         Correction *correction) const {
     if (DEBUG_DICT) {
         AKLOGI("initSuggest");
-        DUMP_WORD_INT(codes, inputLength);
+        DUMP_WORD_INT(codes, inputSize);
     }
-    correction->initInputParams(proximityInfo, codes, inputLength, xCoordinates, yCoordinates);
-    const int maxDepth = min(inputLength * MAX_DEPTH_MULTIPLIER, MAX_WORD_LENGTH);
-    correction->initCorrection(proximityInfo, inputLength, maxDepth);
+    correction->initInputParams(proximityInfo, codes, inputSize, xCoordinates, yCoordinates);
+    const int maxDepth = min(inputSize * MAX_DEPTH_MULTIPLIER, MAX_WORD_LENGTH);
+    correction->initCorrection(proximityInfo, inputSize, maxDepth);
 }
 
 static const char QUOTE = '\'';
@@ -321,15 +321,15 @@
 void UnigramDictionary::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,
+        const bool useFullEditDistance, const int inputSize,
         Correction *correction, WordsPriorityQueuePool *queuePool) const {
-    initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, inputLength, correction);
-    getSuggestionCandidates(useFullEditDistance, inputLength, bigramMap, bigramFilter, correction,
+    initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, inputSize, correction);
+    getSuggestionCandidates(useFullEditDistance, inputSize, bigramMap, bigramFilter, correction,
             queuePool, true /* doAutoCompletion */, DEFAULT_MAX_ERRORS, FIRST_WORD_INDEX);
 }
 
 void UnigramDictionary::getSuggestionCandidates(const bool useFullEditDistance,
-        const int inputLength, const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
+        const int inputSize, const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
         Correction *correction, WordsPriorityQueuePool *queuePool,
         const bool doAutoCompletion, const int maxErrors, const int currentWordIndex) const {
     uint8_t totalTraverseCount = correction->pushAndGetTotalTraverseCount();
@@ -351,7 +351,7 @@
     int childCount = BinaryFormat::getGroupCountAndForwardPointer(DICT_ROOT, &rootPosition);
     int outputIndex = 0;
 
-    correction->initCorrectionState(rootPosition, childCount, (inputLength <= 0));
+    correction->initCorrectionState(rootPosition, childCount, (inputSize <= 0));
 
     // Depth first search
     while (outputIndex >= 0) {
@@ -390,27 +390,42 @@
         WordsPriorityQueue *masterQueue = queuePool->getMasterQueue();
         const int finalProbability =
                 correction->getFinalProbability(probability, &wordPointer, &wordLength);
-        if (finalProbability != NOT_A_PROBABILITY) {
-            addWord(wordPointer, wordLength, finalProbability, masterQueue);
 
-            const int shortcutProbability = finalProbability > 0 ? finalProbability - 1 : 0;
-            // Please note that the shortcut candidates will be added to the master queue only.
-            TerminalAttributes::ShortcutIterator iterator =
-                    terminalAttributes.getShortcutIterator();
-            while (iterator.hasNextShortcutTarget()) {
-                // TODO: addWord only supports weak ordering, meaning we have no means
-                // to control the order of the shortcuts relative to one another or to the word.
-                // We need to either modulate the probability of each shortcut according
-                // to its own shortcut probability or to make the queue
-                // so that the insert order is protected inside the queue for words
-                // with the same score. For the moment we use -1 to make sure the shortcut will
-                // never be in front of the word.
-                uint16_t shortcutTarget[MAX_WORD_LENGTH_INTERNAL];
-                const int shortcutTargetStringLength = iterator.getNextShortcutTarget(
-                        MAX_WORD_LENGTH_INTERNAL, shortcutTarget);
-                addWord(shortcutTarget, shortcutTargetStringLength, shortcutProbability,
-                        masterQueue);
+        if (0 != finalProbability) {
+            // If the probability is 0, we don't want to add this word. However we still
+            // want to add its shortcuts (including a possible whitelist entry) if any.
+            addWord(wordPointer, wordLength, finalProbability, masterQueue,
+                    Dictionary::KIND_CORRECTION);
+        }
+
+        const int shortcutProbability = finalProbability > 0 ? finalProbability - 1 : 0;
+        // Please note that the shortcut candidates will be added to the master queue only.
+        TerminalAttributes::ShortcutIterator iterator =
+                terminalAttributes.getShortcutIterator();
+        while (iterator.hasNextShortcutTarget()) {
+            // TODO: addWord only supports weak ordering, meaning we have no means
+            // to control the order of the shortcuts relative to one another or to the word.
+            // We need to either modulate the probability of each shortcut according
+            // to its own shortcut probability or to make the queue
+            // so that the insert order is protected inside the queue for words
+            // with the same score. For the moment we use -1 to make sure the shortcut will
+            // never be in front of the word.
+            uint16_t shortcutTarget[MAX_WORD_LENGTH_INTERNAL];
+            int shortcutFrequency;
+            const int shortcutTargetStringLength = iterator.getNextShortcutTarget(
+                    MAX_WORD_LENGTH_INTERNAL, shortcutTarget, &shortcutFrequency);
+            int shortcutScore;
+            int kind;
+            if (shortcutFrequency == BinaryFormat::WHITELIST_SHORTCUT_FREQUENCY
+                    && correction->sameAsTyped()) {
+                shortcutScore = S_INT_MAX;
+                kind = Dictionary::KIND_WHITELIST;
+            } else {
+                shortcutScore = shortcutProbability;
+                kind = Dictionary::KIND_CORRECTION;
             }
+            addWord(shortcutTarget, shortcutTargetStringLength, shortcutScore,
+                    masterQueue, kind);
         }
     }
 
@@ -424,14 +439,14 @@
         }
         const int finalProbability = correction->getFinalProbabilityForSubQueue(
                 probability, &wordPointer, &wordLength, inputIndex);
-        addWord(wordPointer, wordLength, finalProbability, subQueue);
+        addWord(wordPointer, wordLength, finalProbability, subQueue, Dictionary::KIND_CORRECTION);
     }
 }
 
 int UnigramDictionary::getSubStringSuggestion(
         ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates,
         const int *codes, const bool useFullEditDistance, Correction *correction,
-        WordsPriorityQueuePool *queuePool, const int inputLength,
+        WordsPriorityQueuePool *queuePool, const int inputSize,
         const bool hasAutoCorrectionCandidate, const int currentWordIndex,
         const int inputWordStartPos, const int inputWordLength,
         const int outputWordStartPos, const bool isSpaceProximity, int *freqArray,
@@ -482,7 +497,7 @@
     int nextWordLength = 0;
     // TODO: Optimize init suggestion
     initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes,
-            inputLength, correction);
+            inputSize, correction);
 
     unsigned short word[MAX_WORD_LENGTH_INTERNAL];
     int freq = getMostFrequentWordLike(
@@ -551,7 +566,7 @@
         *outputWordLength = tempOutputWordLength;
     }
 
-    if ((inputWordStartPos + inputWordLength) < inputLength) {
+    if ((inputWordStartPos + inputWordLength) < inputSize) {
         if (outputWordStartPos + nextWordLength >= MAX_WORD_LENGTH) {
             return FLAG_MULTIPLE_SUGGEST_SKIP;
         }
@@ -570,16 +585,17 @@
                         freqArray[i], wordLengthArray[i]);
             }
             AKLOGI("Split two words: freq = %d, length = %d, %d, isSpace ? %d", pairFreq,
-                    inputLength, tempOutputWordLength, isSpaceProximity);
+                    inputSize, tempOutputWordLength, isSpaceProximity);
         }
-        addWord(outputWord, tempOutputWordLength, pairFreq, queuePool->getMasterQueue());
+        addWord(outputWord, tempOutputWordLength, pairFreq, queuePool->getMasterQueue(),
+                Dictionary::KIND_CORRECTION);
     }
     return FLAG_MULTIPLE_SUGGEST_CONTINUE;
 }
 
 void UnigramDictionary::getMultiWordsSuggestionRec(ProximityInfo *proximityInfo,
         const int *xcoordinates, const int *ycoordinates, const int *codes,
-        const bool useFullEditDistance, const int inputLength,
+        const bool useFullEditDistance, const int inputSize,
         Correction *correction, WordsPriorityQueuePool *queuePool,
         const bool hasAutoCorrectionCandidate, const int startInputPos, const int startWordIndex,
         const int outputWordLength, int *freqArray, int *wordLengthArray,
@@ -590,11 +606,11 @@
     }
     if (startWordIndex >= 1
             && (hasAutoCorrectionCandidate
-                    || inputLength < MIN_INPUT_LENGTH_FOR_THREE_OR_MORE_WORDS_CORRECTION)) {
+                    || inputSize < MIN_INPUT_LENGTH_FOR_THREE_OR_MORE_WORDS_CORRECTION)) {
         // Do not suggest 3+ words if already has auto correction candidate
         return;
     }
-    for (int i = startInputPos + 1; i < inputLength; ++i) {
+    for (int i = startInputPos + 1; i < inputSize; ++i) {
         if (DEBUG_CORRECTION_FREQ) {
             AKLOGI("Multi words(%d), start in %d sep %d start out %d",
                     startWordIndex, startInputPos, i, outputWordLength);
@@ -605,7 +621,7 @@
         int inputWordStartPos = startInputPos;
         int inputWordLength = i - startInputPos;
         const int suggestionFlag = getSubStringSuggestion(proximityInfo, xcoordinates, ycoordinates,
-                codes, useFullEditDistance, correction, queuePool, inputLength,
+                codes, useFullEditDistance, correction, queuePool, inputSize,
                 hasAutoCorrectionCandidate, startWordIndex, inputWordStartPos, inputWordLength,
                 outputWordLength, true /* not used */, freqArray, wordLengthArray, outputWord,
                 &tempOutputWordLength);
@@ -622,14 +638,14 @@
         // Next word
         // Missing space
         inputWordStartPos = i;
-        inputWordLength = inputLength - i;
+        inputWordLength = inputSize - i;
         if(getSubStringSuggestion(proximityInfo, xcoordinates, ycoordinates, codes,
-                useFullEditDistance, correction, queuePool, inputLength, hasAutoCorrectionCandidate,
+                useFullEditDistance, correction, queuePool, inputSize, hasAutoCorrectionCandidate,
                 startWordIndex + 1, inputWordStartPos, inputWordLength, tempOutputWordLength,
                 false /* missing space */, freqArray, wordLengthArray, outputWord, 0)
                         != FLAG_MULTIPLE_SUGGEST_CONTINUE) {
             getMultiWordsSuggestionRec(proximityInfo, xcoordinates, ycoordinates, codes,
-                    useFullEditDistance, inputLength, correction, queuePool,
+                    useFullEditDistance, inputSize, correction, queuePool,
                     hasAutoCorrectionCandidate, inputWordStartPos, startWordIndex + 1,
                     tempOutputWordLength, freqArray, wordLengthArray, outputWord);
         }
@@ -652,7 +668,7 @@
             AKLOGI("Do mistyped space correction");
         }
         getSubStringSuggestion(proximityInfo, xcoordinates, ycoordinates, codes,
-                useFullEditDistance, correction, queuePool, inputLength, hasAutoCorrectionCandidate,
+                useFullEditDistance, correction, queuePool, inputSize, hasAutoCorrectionCandidate,
                 startWordIndex + 1, inputWordStartPos, inputWordLength, tempOutputWordLength,
                 true /* mistyped space */, freqArray, wordLengthArray, outputWord, 0);
     }
@@ -660,10 +676,10 @@
 
 void UnigramDictionary::getSplitMultipleWordsSuggestions(ProximityInfo *proximityInfo,
         const int *xcoordinates, const int *ycoordinates, const int *codes,
-        const bool useFullEditDistance, const int inputLength,
+        const bool useFullEditDistance, const int inputSize,
         Correction *correction, WordsPriorityQueuePool *queuePool,
         const bool hasAutoCorrectionCandidate) const {
-    if (inputLength >= MAX_WORD_LENGTH) return;
+    if (inputSize >= MAX_WORD_LENGTH) return;
     if (DEBUG_DICT) {
         AKLOGI("--- Suggest multiple words");
     }
@@ -676,7 +692,7 @@
     const int startInputPos = 0;
     const int startWordIndex = 0;
     getMultiWordsSuggestionRec(proximityInfo, xcoordinates, ycoordinates, codes,
-            useFullEditDistance, inputLength, correction, queuePool, hasAutoCorrectionCandidate,
+            useFullEditDistance, inputSize, correction, queuePool, hasAutoCorrectionCandidate,
             startInputPos, startWordIndex, outputWordLength, freqArray, wordLengthArray,
             outputWord);
 }
@@ -684,13 +700,13 @@
 // Wrapper for getMostFrequentWordLikeInner, which matches it to the previous
 // interface.
 inline int UnigramDictionary::getMostFrequentWordLike(const int startInputIndex,
-        const int inputLength, Correction *correction, unsigned short *word) const {
-    uint16_t inWord[inputLength];
+        const int inputSize, Correction *correction, unsigned short *word) const {
+    uint16_t inWord[inputSize];
 
-    for (int i = 0; i < inputLength; ++i) {
+    for (int i = 0; i < inputSize; ++i) {
         inWord[i] = (uint16_t)correction->getPrimaryCharAt(startInputIndex + i);
     }
-    return getMostFrequentWordLikeInner(inWord, inputLength, word);
+    return getMostFrequentWordLikeInner(inWord, inputSize, word);
 }
 
 // This function will take the position of a character array within a CharGroup,
diff --git a/native/jni/src/unigram_dictionary.h b/native/jni/src/unigram_dictionary.h
index 6083f01..2c66222 100644
--- a/native/jni/src/unigram_dictionary.h
+++ b/native/jni/src/unigram_dictionary.h
@@ -53,7 +53,7 @@
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(UnigramDictionary);
     void getWordSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
-            const int *ycoordinates, const int *codes, const int inputLength,
+            const int *ycoordinates, const int *codes, const int inputSize,
             const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
             const bool useFullEditDistance, Correction *correction,
             WordsPriorityQueuePool *queuePool) const;
@@ -72,16 +72,16 @@
             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,
+            const uint8_t *bigramFilter, const bool useFullEditDistance, const int inputSize,
             Correction *correction, WordsPriorityQueuePool *queuePool) const;
     void getSuggestionCandidates(
-            const bool useFullEditDistance, const int inputLength,
+            const bool useFullEditDistance, const int inputSize,
             const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
             Correction *correction, WordsPriorityQueuePool *queuePool, const bool doAutoCompletion,
             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,
+            const bool useFullEditDistance, const int inputSize,
             Correction *correction, WordsPriorityQueuePool *queuePool,
             const bool hasAutoCorrectionCandidate) const;
     void onTerminal(const int freq, const TerminalAttributes& terminalAttributes,
@@ -92,21 +92,21 @@
             const uint8_t *bigramFilter, Correction *correction, int *newCount,
             int *newChildPosition, int *nextSiblingPosition, WordsPriorityQueuePool *queuePool,
             const int currentWordIndex) const;
-    int getMostFrequentWordLike(const int startInputIndex, const int inputLength,
+    int getMostFrequentWordLike(const int startInputIndex, const int inputSize,
             Correction *correction, unsigned short *word) const;
     int getMostFrequentWordLikeInner(const uint16_t *const inWord, const int length,
             short unsigned int *outWord) const;
     int getSubStringSuggestion(
             ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates,
             const int *codes, const bool useFullEditDistance, Correction *correction,
-            WordsPriorityQueuePool *queuePool, const int inputLength,
+            WordsPriorityQueuePool *queuePool, const int inputSize,
             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) const;
     void getMultiWordsSuggestionRec(ProximityInfo *proximityInfo,
             const int *xcoordinates, const int *ycoordinates, const int *codes,
-            const bool useFullEditDistance, const int inputLength,
+            const bool useFullEditDistance, const int inputSize,
             Correction *correction, WordsPriorityQueuePool *queuePool,
             const bool hasAutoCorrectionCandidate, const int startPos, const int startWordIndex,
             const int outputWordLength, int *freqArray, int *wordLengthArray,
diff --git a/native/jni/src/words_priority_queue.h b/native/jni/src/words_priority_queue.h
index c0dedb5..19efa5d 100644
--- a/native/jni/src/words_priority_queue.h
+++ b/native/jni/src/words_priority_queue.h
@@ -33,30 +33,31 @@
         unsigned short mWord[MAX_WORD_LENGTH_INTERNAL];
         int mWordLength;
         bool mUsed;
+        int mType;
 
-        void setParams(int score, unsigned short *word, int wordLength) {
+        void setParams(int score, unsigned short *word, int wordLength, int type) {
             mScore = score;
             mWordLength = wordLength;
             memcpy(mWord, word, sizeof(unsigned short) * wordLength);
             mUsed = true;
+            mType = type;
         }
     };
 
-    WordsPriorityQueue(int maxWords, int maxWordLength) :
-            MAX_WORDS((unsigned int) maxWords), MAX_WORD_LENGTH(
-                    (unsigned int) maxWordLength) {
-        mSuggestedWords = new SuggestedWord[maxWordLength];
+    WordsPriorityQueue(int maxWords, int maxWordLength)
+            : mSuggestions(), MAX_WORDS(static_cast<unsigned int>(maxWords)),
+              MAX_WORD_LENGTH(static_cast<unsigned int>(maxWordLength)),
+              mSuggestedWords(new SuggestedWord[maxWordLength]), mHighestSuggestedWord(0) {
         for (int i = 0; i < maxWordLength; ++i) {
             mSuggestedWords[i].mUsed = false;
         }
-        mHighestSuggestedWord = 0;
     }
 
-    ~WordsPriorityQueue() {
+    virtual ~WordsPriorityQueue() {
         delete[] mSuggestedWords;
     }
 
-    void push(int score, unsigned short *word, int wordLength) {
+    void push(int score, unsigned short *word, int wordLength, int type) {
         SuggestedWord *sw = 0;
         if (mSuggestions.size() >= MAX_WORDS) {
             sw = mSuggestions.top();
@@ -69,9 +70,9 @@
             }
         }
         if (sw == 0) {
-            sw = getFreeSuggestedWord(score, word, wordLength);
+            sw = getFreeSuggestedWord(score, word, wordLength, type);
         } else {
-            sw->setParams(score, word, wordLength);
+            sw->setParams(score, word, wordLength, type);
         }
         if (sw == 0) {
             AKLOGE("SuggestedWord is accidentally null.");
@@ -94,7 +95,7 @@
     }
 
     int outputSuggestions(const unsigned short *before, const int beforeLength,
-            int *frequencies, unsigned short *outputChars) {
+            int *frequencies, unsigned short *outputChars, int* outputTypes) {
         mHighestSuggestedWord = 0;
         const unsigned int size = min(
               MAX_WORDS, static_cast<unsigned int>(mSuggestions.size()));
@@ -127,7 +128,7 @@
                 }
             }
             if (maxIndex > 0 && nsMaxSw) {
-                memmove(&swBuffer[1], &swBuffer[0], maxIndex * sizeof(SuggestedWord*));
+                memmove(&swBuffer[1], &swBuffer[0], maxIndex * sizeof(SuggestedWord *));
                 swBuffer[0] = nsMaxSw;
             }
         }
@@ -138,11 +139,12 @@
                 continue;
             }
             const unsigned int wordLength = sw->mWordLength;
-            char *targetAdr = (char*) outputChars + i * MAX_WORD_LENGTH * sizeof(short);
+            unsigned short *targetAddress = outputChars + i * MAX_WORD_LENGTH;
             frequencies[i] = sw->mScore;
-            memcpy(targetAdr, sw->mWord, (wordLength) * sizeof(short));
+            outputTypes[i] = sw->mType;
+            memcpy(targetAddress, sw->mWord, wordLength * sizeof(unsigned short));
             if (wordLength < MAX_WORD_LENGTH) {
-                ((unsigned short*) targetAdr)[wordLength] = 0;
+                targetAddress[wordLength] = 0;
             }
             sw->mUsed = false;
         }
@@ -191,10 +193,10 @@
     };
 
     SuggestedWord *getFreeSuggestedWord(int score, unsigned short *word,
-            int wordLength) {
+            int wordLength, int type) {
         for (unsigned int i = 0; i < MAX_WORD_LENGTH; ++i) {
             if (!mSuggestedWords[i].mUsed) {
-                mSuggestedWords[i].setParams(score, word, wordLength);
+                mSuggestedWords[i].setParams(score, word, wordLength, type);
                 return &mSuggestedWords[i];
             }
         }
@@ -219,7 +221,7 @@
                 before, beforeLength, word, wordLength, score);
     }
 
-    typedef std::priority_queue<SuggestedWord*, std::vector<SuggestedWord*>,
+    typedef std::priority_queue<SuggestedWord *, std::vector<SuggestedWord *>,
             wordComparator> Suggestions;
     Suggestions mSuggestions;
     const unsigned int MAX_WORDS;
diff --git a/native/jni/src/words_priority_queue_pool.h b/native/jni/src/words_priority_queue_pool.h
index 3888729..c5de979 100644
--- a/native/jni/src/words_priority_queue_pool.h
+++ b/native/jni/src/words_priority_queue_pool.h
@@ -24,9 +24,10 @@
 
 class WordsPriorityQueuePool {
  public:
-    WordsPriorityQueuePool(int mainQueueMaxWords, int subQueueMaxWords, int maxWordLength) {
-        // Note: using placement new() requires the caller to call the destructor explicitly.
-        mMasterQueue = new(mMasterQueueBuf) WordsPriorityQueue(mainQueueMaxWords, maxWordLength);
+    WordsPriorityQueuePool(int mainQueueMaxWords, int subQueueMaxWords, int maxWordLength)
+            // Note: using placement new() requires the caller to call the destructor explicitly.
+            : mMasterQueue(new(mMasterQueueBuf) WordsPriorityQueue(
+                      mainQueueMaxWords, maxWordLength)) {
         for (int i = 0, subQueueBufOffset = 0;
                 i < MULTIPLE_WORDS_SUGGESTION_MAX_WORDS * SUB_QUEUE_MAX_COUNT;
                 ++i, subQueueBufOffset += sizeof(WordsPriorityQueue)) {
@@ -85,11 +86,11 @@
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(WordsPriorityQueuePool);
+    char mMasterQueueBuf[sizeof(WordsPriorityQueue)];
+    char mSubQueueBuf[SUB_QUEUE_MAX_COUNT * MULTIPLE_WORDS_SUGGESTION_MAX_WORDS
+            * sizeof(WordsPriorityQueue)];
     WordsPriorityQueue *mMasterQueue;
     WordsPriorityQueue *mSubQueues[SUB_QUEUE_MAX_COUNT * MULTIPLE_WORDS_SUGGESTION_MAX_WORDS];
-    char mMasterQueueBuf[sizeof(WordsPriorityQueue)];
-    char mSubQueueBuf[MULTIPLE_WORDS_SUGGESTION_MAX_WORDS
-                      * SUB_QUEUE_MAX_COUNT * sizeof(WordsPriorityQueue)];
 };
 } // namespace latinime
 #endif // LATINIME_WORDS_PRIORITY_QUEUE_POOL_H
diff --git a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
index 87501ee..bc50439 100644
--- a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
@@ -22,6 +22,7 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.AdditionalSubtype;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.ImfUtils;
 import com.android.inputmethod.latin.StringUtils;
 import com.android.inputmethod.latin.SubtypeLocale;
@@ -31,7 +32,7 @@
 
 public class SpacebarTextTests extends AndroidTestCase {
     // Locale to subtypes list.
-    private final ArrayList<InputMethodSubtype> mSubtypesList = new ArrayList<InputMethodSubtype>();
+    private final ArrayList<InputMethodSubtype> mSubtypesList = CollectionUtils.newArrayList();
 
     private Resources mRes;
 
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
index 3dc4543..1346c00 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
@@ -19,6 +19,8 @@
 import android.app.Instrumentation;
 import android.test.InstrumentationTestCase;
 
+import com.android.inputmethod.latin.CollectionUtils;
+
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -42,7 +44,7 @@
     }
 
     private static String[] getAllResourceIdNames(final Class<?> resourceIdClass) {
-        final ArrayList<String> names = new ArrayList<String>();
+        final ArrayList<String> names = CollectionUtils.newArrayList();
         for (final Field field : resourceIdClass.getFields()) {
             if (field.getType() == Integer.TYPE) {
                 names.add(field.getName());
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
index 99fbc96..8fed28f 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
@@ -19,7 +19,7 @@
 import android.test.AndroidTestCase;
 
 public class PointerTrackerQueueTests extends AndroidTestCase {
-    public static class Element implements PointerTrackerQueue.ElementActions {
+    public static class Element implements PointerTrackerQueue.Element {
         public static int sPhantomUpCount;
         public static final long NOT_HAPPENED = -1;
 
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictIOTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictIOTests.java
new file mode 100644
index 0000000..0094db8
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictIOTests.java
@@ -0,0 +1,224 @@
+/*
+ * 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 com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import android.test.AndroidTestCase;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * Unit tests for BinaryDictInputOutput
+ */
+public class BinaryDictIOTests extends AndroidTestCase {
+    private static final String TAG = BinaryDictIOTests.class.getSimpleName();
+    private static final int MAX_UNIGRAMS = 1000;
+    private static final int UNIGRAM_FREQ = 10;
+    private static final int BIGRAM_FREQ = 50;
+
+    private static final String[] CHARACTERS =
+        {
+        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+        "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
+        };
+
+    /**
+     * Generates a random word.
+     */
+    private String generateWord(final int value) {
+        final int lengthOfChars = CHARACTERS.length;
+        StringBuilder builder = new StringBuilder("a");
+        long lvalue = Math.abs((long)value);
+        while (lvalue > 0) {
+            builder.append(CHARACTERS[(int)(lvalue % lengthOfChars)]);
+            lvalue /= lengthOfChars;
+        }
+        return builder.toString();
+    }
+
+    private List<String> generateWords(final int number, final Random random) {
+        final Set<String> wordSet = CollectionUtils.newHashSet();
+        while (wordSet.size() < number) {
+            wordSet.add(generateWord(random.nextInt()));
+        }
+        return new ArrayList<String>(wordSet);
+    }
+
+    private void addUnigrams(final int number,
+            final FusionDictionary dict,
+            final List<String> words) {
+        for (int i = 0; i < number; ++i) {
+            final String word = words.get(i);
+            dict.add(word, UNIGRAM_FREQ, null);
+        }
+    }
+
+    private void addBigrams(final FusionDictionary dict,
+            final List<String> words,
+            final SparseArray<List<Integer>> sparseArray) {
+        for (int i = 0; i < sparseArray.size(); ++i) {
+            final int w1 = sparseArray.keyAt(i);
+            for (int w2 : sparseArray.valueAt(i)) {
+                dict.setBigram(words.get(w1), words.get(w2), BIGRAM_FREQ);
+            }
+        }
+    }
+
+    private long timeWritingDictToFile(final String fileName,
+            final FusionDictionary dict) {
+
+        final File file = new File(getContext().getFilesDir(), fileName);
+        long now = -1, diff = -1;
+
+        try {
+            final FileOutputStream out = new FileOutputStream(file);
+
+            now = System.currentTimeMillis();
+            BinaryDictInputOutput.writeDictionaryBinary(out, dict, 2);
+            diff = System.currentTimeMillis() - now;
+
+            out.flush();
+            out.close();
+        } catch (IOException e) {
+            Log.e(TAG, "IO exception while writing file: " + e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "UnsupportedFormatException: " + e);
+        }
+
+        return diff;
+    }
+
+    private void checkDictionary(final FusionDictionary dict,
+            final List<String> words,
+            final SparseArray<List<Integer>> bigrams) {
+        assertNotNull(dict);
+
+        // check unigram
+        for (final String word : words) {
+            final CharGroup cg = FusionDictionary.findWordInTree(dict.mRoot, word);
+            assertNotNull(cg);
+        }
+
+        // check bigram
+        for (int i = 0; i < bigrams.size(); ++i) {
+            final int w1 = bigrams.keyAt(i);
+            for (final int w2 : bigrams.valueAt(i)) {
+                final CharGroup cg = FusionDictionary.findWordInTree(dict.mRoot, words.get(w1));
+                assertNotNull(words.get(w1) + "," + words.get(w2), cg.getBigram(words.get(w2)));
+            }
+        }
+    }
+
+    private long timeReadingAndCheckDict(final String fileName,
+            final List<String> words,
+            final SparseArray<List<Integer>> bigrams) {
+
+        long now, diff = -1;
+
+        try {
+            final File file = new File(getContext().getFilesDir(), fileName);
+            final FileInputStream inStream = new FileInputStream(file);
+            final ByteBuffer buffer = inStream.getChannel().map(
+                    FileChannel.MapMode.READ_ONLY, 0, file.length());
+
+            now = System.currentTimeMillis();
+
+            final FusionDictionary dict =
+                    BinaryDictInputOutput.readDictionaryBinary(buffer, null);
+
+            diff = System.currentTimeMillis() - now;
+
+            checkDictionary(dict, words, bigrams);
+            return diff;
+
+        } catch (IOException e) {
+            Log.e(TAG, "raise IOException while reading file " + e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "Unsupported format: " + e);
+        }
+
+        return diff;
+    }
+
+    private String runReadAndWrite(final List<String> words,
+            final SparseArray<List<Integer>> bigrams,
+            final String message) {
+        final FusionDictionary dict = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(
+                        new HashMap<String,String>(), false, false));
+
+        final String fileName = generateWord((int)System.currentTimeMillis()) + ".dict";
+
+        addUnigrams(words.size(), dict, words);
+        addBigrams(dict, words, bigrams);
+        // check original dictionary
+        checkDictionary(dict, words, bigrams);
+
+        final long write = timeWritingDictToFile(fileName, dict);
+        final long read = timeReadingAndCheckDict(fileName, words, bigrams);
+        deleteFile(fileName);
+
+        return "PROF: read=" + read + "ms, write=" + write + "ms    :" + message;
+    }
+
+    private void deleteFile(final String fileName) {
+        final File file = new File(getContext().getFilesDir(), fileName);
+        file.delete();
+    }
+
+    public void testReadAndWrite() {
+        final List<String> results = new ArrayList<String>();
+
+        final Random random = new Random(123456);
+        final List<String> words = generateWords(MAX_UNIGRAMS, random);
+        final SparseArray<List<Integer>> emptyArray = CollectionUtils.newSparseArray();
+
+        final SparseArray<List<Integer>> chain = CollectionUtils.newSparseArray();
+        for (int i = 0; i < words.size(); ++i) chain.put(i, new ArrayList<Integer>());
+        for (int i = 1; i < words.size(); ++i) chain.get(i-1).add(i);
+
+        final SparseArray<List<Integer>> star = CollectionUtils.newSparseArray();
+        final List<Integer> list0 = CollectionUtils.newArrayList();
+        star.put(0, list0);
+        for (int i = 1; i < words.size(); ++i) star.get(0).add(i);
+
+        results.add(runReadAndWrite(words, emptyArray, "only unigram"));
+        results.add(runReadAndWrite(words, chain, "chain"));
+        results.add(runReadAndWrite(words, star, "star"));
+
+        for (final String result : results) {
+            Log.d(TAG, result);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/FusionDictionaryTests.java b/tests/src/com/android/inputmethod/latin/FusionDictionaryTests.java
new file mode 100644
index 0000000..8ecdcc3
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/FusionDictionaryTests.java
@@ -0,0 +1,48 @@
+/*
+ * 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.test.AndroidTestCase;
+
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+
+import java.util.HashMap;
+
+/**
+ * Unit test for FusionDictionary
+ */
+public class FusionDictionaryTests extends AndroidTestCase {
+    public void testFindWordInTree() {
+        FusionDictionary dict = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(new HashMap<String,String>(), false, false));
+
+        dict.add("abc", 10, null);
+        assertNull(FusionDictionary.findWordInTree(dict.mRoot, "aaa"));
+        assertNotNull(FusionDictionary.findWordInTree(dict.mRoot, "abc"));
+
+        dict.add("aa", 10, null);
+        assertNull(FusionDictionary.findWordInTree(dict.mRoot, "aaa"));
+        assertNotNull(FusionDictionary.findWordInTree(dict.mRoot, "aa"));
+
+        dict.add("babcd", 10, null);
+        dict.add("bacde", 10, null);
+        assertNull(FusionDictionary.findWordInTree(dict.mRoot, "ba"));
+        assertNotNull(FusionDictionary.findWordInTree(dict.mRoot, "babcd"));
+        assertNotNull(FusionDictionary.findWordInTree(dict.mRoot, "bacde"));
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/InputPointersTests.java b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
index 6f04f3e..cc55076 100644
--- a/tests/src/com/android/inputmethod/latin/InputPointersTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
@@ -18,6 +18,8 @@
 
 import android.test.AndroidTestCase;
 
+import java.util.Arrays;
+
 public class InputPointersTests extends AndroidTestCase {
     private static final int DEFAULT_CAPACITY = 48;
 
@@ -162,6 +164,61 @@
                 src.getTimes(), 0, dst.getTimes(), dstLen, srcLen);
     }
 
+    public void testAppendResizableIntArray() {
+        final int srcLen = 100;
+        final int srcPointerId = 1;
+        final int[] srcPointerIds = new int[srcLen];
+        Arrays.fill(srcPointerIds, srcPointerId);
+        final ResizableIntArray srcTimes = new ResizableIntArray(DEFAULT_CAPACITY);
+        final ResizableIntArray srcXCoords = new ResizableIntArray(DEFAULT_CAPACITY);
+        final ResizableIntArray srcYCoords= new ResizableIntArray(DEFAULT_CAPACITY);
+        for (int i = 0; i < srcLen; i++) {
+            srcTimes.add(i * 2);
+            srcXCoords.add(i * 3);
+            srcYCoords.add(i * 4);
+        }
+        final int dstLen = 50;
+        final InputPointers dst = new InputPointers(DEFAULT_CAPACITY);
+        for (int i = 0; i < dstLen; i++) {
+            final int value = -i - 1;
+            dst.addPointer(value * 4, value * 3, value * 2, value);
+        }
+        final InputPointers dstCopy = new InputPointers(DEFAULT_CAPACITY);
+        dstCopy.copy(dst);
+
+        dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, 0, 0);
+        assertEquals("size after append zero", dstLen, dst.getPointerSize());
+        assertArrayEquals("xCoordinates after append zero",
+                dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLen);
+        assertArrayEquals("yCoordinates after append zero",
+                dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLen);
+        assertArrayEquals("pointerIds after append zero",
+                dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLen);
+        assertArrayEquals("times after append zero",
+                dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLen);
+
+        dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, 0, srcLen);
+        assertEquals("size after append", dstLen + srcLen, dst.getPointerSize());
+        assertTrue("primitive length after append",
+                dst.getPointerIds().length >= dstLen + srcLen);
+        assertArrayEquals("original xCoordinates values after append",
+                dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLen);
+        assertArrayEquals("original yCoordinates values after append",
+                dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLen);
+        assertArrayEquals("original pointerIds values after append",
+                dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLen);
+        assertArrayEquals("original times values after append",
+                dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLen);
+        assertArrayEquals("appended xCoordinates values after append",
+                srcXCoords.getPrimitiveArray(), 0, dst.getXCoordinates(), dstLen, srcLen);
+        assertArrayEquals("appended yCoordinates values after append",
+                srcYCoords.getPrimitiveArray(), 0, dst.getYCoordinates(), dstLen, srcLen);
+        assertArrayEquals("appended pointerIds values after append",
+                srcPointerIds, 0, dst.getPointerIds(), dstLen, srcLen);
+        assertArrayEquals("appended times values after append",
+                srcTimes.getPrimitiveArray(), 0, dst.getTimes(), dstLen, srcLen);
+    }
+
     private static void assertArrayEquals(String message, int[] expecteds, int expectedPos,
             int[] actuals, int actualPos, int length) {
         if (expecteds == null && actuals == null) {
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index c672d51..ffd95f5 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -39,7 +39,6 @@
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardActionListener;
 
 import java.util.HashMap;
 
@@ -136,7 +135,6 @@
         mLatinIME.onCreateInputView();
         mLatinIME.onStartInputView(ei, false);
         mInputConnection = ic;
-        mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
         changeLanguage("en_US");
     }
 
@@ -222,9 +220,7 @@
                 return;
             }
         }
-        mLatinIME.onCodeInput(codePoint,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+        mLatinIME.onCodeInput(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         //mLatinIME.onReleaseKey(codePoint, false);
     }
 
@@ -256,13 +252,13 @@
             fail("InputMethodSubtype for locale " + locale + " is not enabled");
         }
         SubtypeSwitcher.getInstance().updateSubtype(subtype);
+        mLatinIME.loadKeyboard();
+        mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
         waitForDictionaryToBeLoaded();
     }
 
     protected void pickSuggestionManually(final int index, final CharSequence suggestion) {
-        mLatinIME.pickSuggestionManually(index, suggestion,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+        mLatinIME.pickSuggestionManually(index, suggestion);
     }
 
     // Helper to avoid writing the try{}catch block each time
diff --git a/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java b/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java
index c70c2fd..52a3745 100644
--- a/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java
+++ b/tests/src/com/android/inputmethod/latin/SubtypeLocaleTests.java
@@ -28,7 +28,7 @@
 
 public class SubtypeLocaleTests extends AndroidTestCase {
     // Locale to subtypes list.
-    private final ArrayList<InputMethodSubtype> mSubtypesList = new ArrayList<InputMethodSubtype>();
+    private final ArrayList<InputMethodSubtype> mSubtypesList = CollectionUtils.newArrayList();
 
     private Resources mRes;
 
diff --git a/tools/dicttool/Android.mk b/tools/dicttool/Android.mk
index e9c11ac..b0b47af 100644
--- a/tools/dicttool/Android.mk
+++ b/tools/dicttool/Android.mk
@@ -24,9 +24,8 @@
         $(filter-out $(addprefix %/, $(notdir $(LOCAL_TOOL_SRC_FILES))), $(LOCAL_MAIN_SRC_FILES)) \
         $(call all-java-files-under,tests)
 LOCAL_JAR_MANIFEST := etc/manifest.txt
-LOCAL_MODULE := dicttool
+LOCAL_MODULE := dicttool_aosp
 LOCAL_JAVA_LIBRARIES := junit
-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
index 1eab70f..0c611b7 100644
--- a/tools/dicttool/etc/Android.mk
+++ b/tools/dicttool/etc/Android.mk
@@ -15,6 +15,5 @@
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
-LOCAL_MODULE_TAGS := eng
-LOCAL_PREBUILT_EXECUTABLES := dicttool makedict
+LOCAL_PREBUILT_EXECUTABLES := dicttool_aosp makedict_aosp
 include $(BUILD_HOST_PREBUILT)
diff --git a/tools/dicttool/etc/dicttool b/tools/dicttool/etc/dicttool_aosp
similarity index 98%
rename from tools/dicttool/etc/dicttool
rename to tools/dicttool/etc/dicttool_aosp
index 8a39694..a4879a2 100755
--- a/tools/dicttool/etc/dicttool
+++ b/tools/dicttool/etc/dicttool_aosp
@@ -33,7 +33,7 @@
 prog="${progdir}"/`basename "${prog}"`
 cd "${oldwd}"
 
-jarfile=dicttool.jar
+jarfile=dicttool_aosp.jar
 frameworkdir="$progdir"
 if [ ! -r "$frameworkdir/$jarfile" ]
 then
diff --git a/tools/dicttool/etc/makedict b/tools/dicttool/etc/makedict_aosp
similarity index 96%
rename from tools/dicttool/etc/makedict
rename to tools/dicttool/etc/makedict_aosp
index fffeb23..095c505 100755
--- a/tools/dicttool/etc/makedict
+++ b/tools/dicttool/etc/makedict_aosp
@@ -15,4 +15,4 @@
 
 # Dicttool supports making the dictionary using the 'makedict' command and
 # the same arguments that the old 'makedict' command used to accept.
-dicttool makedict $@
+dicttool_aosp makedict $@
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/AdditionalCommandList.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/AdditionalCommandList.java
new file mode 100644
index 0000000..8d4eb75
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/AdditionalCommandList.java
@@ -0,0 +1,22 @@
+/**
+ * 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 AdditionalCommandList {
+    public static void populate() {
+    }
+}
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/CommandList.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/CommandList.java
new file mode 100644
index 0000000..d16b069
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/CommandList.java
@@ -0,0 +1,26 @@
+/**
+ * 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 CommandList {
+    public static void populate() {
+        Dicttool.addCommand("info", Info.class);
+        Dicttool.addCommand("compress", Compress.Compressor.class);
+        Dicttool.addCommand("uncompress", Compress.Uncompressor.class);
+        Dicttool.addCommand("makedict", Makedict.class);
+    }
+}
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java
index a76ec50..3cb0a12 100644
--- a/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java
@@ -46,46 +46,52 @@
 
     static public class Compressor extends Dicttool.Command {
         public static final String COMMAND = "compress";
-        private static final String SUFFIX = ".compressed";
+        public static final String STDIN_OR_STDOUT = "-";
 
         public Compressor() {
         }
 
         public String getHelp() {
-            return "compress <filename>: Compresses a file using gzip compression";
+            return COMMAND + " <src_filename> <dst_filename>: "
+                    + "Compresses a file using gzip compression";
         }
 
         public void run() throws IOException {
-            if (mArgs.length < 1) {
-                throw new RuntimeException("Not enough arguments for command " + COMMAND);
+            if (mArgs.length > 2) {
+                throw new RuntimeException("Too many arguments for command " + COMMAND);
             }
-            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));
+            final String inFilename = mArgs.length >= 1 ? mArgs[0] : STDIN_OR_STDOUT;
+            final String outFilename = mArgs.length >= 2 ? mArgs[1] : STDIN_OR_STDOUT;
+            final InputStream input = inFilename.equals(STDIN_OR_STDOUT) ? System.in
+                    : new FileInputStream(new File(inFilename));
+            final OutputStream output = outFilename.equals(STDIN_OR_STDOUT) ? System.out
+                    : 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 static final String STDIN_OR_STDOUT = "-";
 
         public Uncompressor() {
         }
 
         public String getHelp() {
-            return "uncompress <filename>: Uncompresses a file compressed with gzip compression";
+            return COMMAND + " <src_filename> <dst_filename>: "
+                    + "Uncompresses a file compressed with gzip compression";
         }
 
         public void run() throws IOException {
-            if (mArgs.length < 1) {
-                throw new RuntimeException("Not enough arguments for command " + COMMAND);
+            if (mArgs.length > 2) {
+                throw new RuntimeException("Too many arguments for command " + COMMAND);
             }
-            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));
+            final String inFilename = mArgs.length >= 1 ? mArgs[0] : STDIN_OR_STDOUT;
+            final String outFilename = mArgs.length >= 2 ? mArgs[1] : STDIN_OR_STDOUT;
+            final InputStream input = inFilename.equals(STDIN_OR_STDOUT) ? System.in
+                    : new FileInputStream(new File(inFilename));
+            final OutputStream output = outFilename.equals(STDIN_OR_STDOUT) ? System.out
+                    : new FileOutputStream(new File(outFilename));
             copy(new GZIPInputStream(input), output);
         }
     }
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/DictionaryMaker.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/DictionaryMaker.java
index 9ebd3bb..fbfc1da 100644
--- a/tools/dicttool/src/android/inputmethod/latin/dicttool/DictionaryMaker.java
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/DictionaryMaker.java
@@ -27,7 +27,8 @@
 import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.util.Arrays;
 import java.util.LinkedList;
 
@@ -112,7 +113,7 @@
 
         public static String getHelp() {
             return "Usage: makedict "
-                    + "[-s <unigrams.xml> [-b <bigrams.xml>] [-c <shortcuts.xml>] "
+                    + "[-s <unigrams.xml> [-b <bigrams.xml>] [-c <shortcuts_and_whitelist.xml>] "
                     + "| -s <binary input>] [-d <binary output format version 2>] "
                     + "[-d1 <binary output format version 1>] [-x <xml output>] [-2]\n"
                     + "\n"
@@ -238,15 +239,30 @@
      */
     private static FusionDictionary readBinaryFile(final String binaryFilename)
             throws FileNotFoundException, IOException, UnsupportedFormatException {
-        final RandomAccessFile inputFile = new RandomAccessFile(binaryFilename, "r");
-        return BinaryDictInputOutput.readDictionaryBinary(inputFile, null);
+        FileInputStream inStream = null;
+
+        try {
+            final File file = new File(binaryFilename);
+            inStream = new FileInputStream(file);
+            final ByteBuffer buffer = inStream.getChannel().map(
+                    FileChannel.MapMode.READ_ONLY, 0, file.length());
+            return BinaryDictInputOutput.readDictionaryBinary(buffer, null);
+        } finally {
+            if (inStream != null) {
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    // do nothing
+                }
+            }
+        }
     }
 
     /**
      * Read a dictionary from a unigram XML file, and optionally a bigram XML file.
      *
      * @param unigramXmlFilename the name of the unigram XML file. May not be null.
-     * @param shortcutXmlFilename the name of the shortcut XML file, or null if there is none.
+     * @param shortcutXmlFilename the name of the shortcut/whitelist XML file, or null if none.
      * @param bigramXmlFilename the name of the bigram XML file. Pass null if there are no bigrams.
      * @return the read dictionary.
      * @throws FileNotFoundException if one of the files can't be found
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java
index c14ce7b..bf417fb 100644
--- a/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java
@@ -32,10 +32,11 @@
     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);
-        sCommands.put("makedict", Makedict.class);
+        CommandList.populate();
+        AdditionalCommandList.populate();
+    }
+    public static void addCommand(final String commandName, final Class<? extends Command> cls) {
+        sCommands.put(commandName, cls);
     }
 
     private static Command getCommandInstance(final String commandName) {
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/XmlDictInputOutput.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
index 8e2e735..9ce8c49 100644
--- a/tools/dicttool/src/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
@@ -90,6 +90,10 @@
 
         public FusionDictionary getFinalDictionary() {
             final FusionDictionary dict = mDictionary;
+            for (final String shortcutOnly : mShortcutsMap.keySet()) {
+                if (dict.hasWord(shortcutOnly)) continue;
+                dict.add(shortcutOnly, 0, mShortcutsMap.get(shortcutOnly));
+            }
             mDictionary = null;
             mShortcutsMap.clear();
             mWord = "";
@@ -179,7 +183,7 @@
                 mSrc = attrs.getValue(uri, SRC_ATTRIBUTE);
             } else if (DST_TAG.equals(localName)) {
                 String dst = attrs.getValue(uri, DST_ATTRIBUTE);
-                int freq = Integer.parseInt(attrs.getValue(uri, DST_FREQ));
+                int freq = getValueFromFreqString(attrs.getValue(uri, DST_FREQ));
                 WeightedString bigram = new WeightedString(dst, freq / XML_TO_MEMORY_RATIO);
                 ArrayList<WeightedString> bigramList = mAssocMap.get(mSrc);
                 if (null == bigramList) bigramList = new ArrayList<WeightedString>();
@@ -188,6 +192,10 @@
             }
         }
 
+        protected int getValueFromFreqString(final String freqString) {
+            return Integer.parseInt(freqString);
+        }
+
         // This may return an empty map, but will never return null.
         public HashMap<String, ArrayList<WeightedString>> getAssocMap() {
             return mAssocMap;
@@ -216,22 +224,40 @@
     }
 
     /**
-     * SAX handler for a shortcut XML file.
+     * SAX handler for a shortcut & whitelist XML file.
      */
-    static private class ShortcutHandler extends AssociativeListHandler {
+    static private class ShortcutAndWhitelistHandler extends AssociativeListHandler {
         private final static String ENTRY_TAG = "entry";
         private final static String ENTRY_ATTRIBUTE = "shortcut";
         private final static String TARGET_TAG = "target";
         private final static String REPLACEMENT_ATTRIBUTE = "replacement";
         private final static String TARGET_PRIORITY_ATTRIBUTE = "priority";
+        private final static String WHITELIST_MARKER = "whitelist";
+        private final static int WHITELIST_FREQ_VALUE = 15;
+        private final static int MIN_FREQ = 0;
+        private final static int MAX_FREQ = 14;
 
-        public ShortcutHandler() {
+        public ShortcutAndWhitelistHandler() {
             super(ENTRY_TAG, ENTRY_ATTRIBUTE, TARGET_TAG, REPLACEMENT_ATTRIBUTE,
                     TARGET_PRIORITY_ATTRIBUTE);
         }
 
+        @Override
+        protected int getValueFromFreqString(final String freqString) {
+            if (WHITELIST_MARKER.equals(freqString)) {
+                return WHITELIST_FREQ_VALUE;
+            } else {
+                final int intValue = super.getValueFromFreqString(freqString);
+                if (intValue < MIN_FREQ || intValue > MAX_FREQ) {
+                    throw new RuntimeException("Shortcut freq out of range. Accepted range is "
+                            + MIN_FREQ + ".." + MAX_FREQ);
+                }
+                return intValue;
+            }
+        }
+
         // As per getAssocMap(), this never returns null.
-        public HashMap<String, ArrayList<WeightedString>> getShortcutMap() {
+        public HashMap<String, ArrayList<WeightedString>> getShortcutAndWhitelistMap() {
             return getAssocMap();
         }
     }
@@ -243,7 +269,7 @@
      * representation.
      *
      * @param unigrams the file to read the data from.
-     * @param shortcuts the file to read the shortcuts from, or null.
+     * @param shortcuts the file to read the shortcuts & whitelist from, or null.
      * @param bigrams the file to read the bigrams from, or null.
      * @return the in-memory representation of the dictionary.
      */
@@ -256,11 +282,12 @@
         final BigramHandler bigramHandler = new BigramHandler();
         if (null != bigrams) parser.parse(bigrams, bigramHandler);
 
-        final ShortcutHandler shortcutHandler = new ShortcutHandler();
-        if (null != shortcuts) parser.parse(shortcuts, shortcutHandler);
+        final ShortcutAndWhitelistHandler shortcutAndWhitelistHandler =
+                new ShortcutAndWhitelistHandler();
+        if (null != shortcuts) parser.parse(shortcuts, shortcutAndWhitelistHandler);
 
         final UnigramHandler unigramHandler =
-                new UnigramHandler(shortcutHandler.getShortcutMap());
+                new UnigramHandler(shortcutAndWhitelistHandler.getShortcutAndWhitelistMap());
         parser.parse(unigrams, unigramHandler);
         final FusionDictionary dict = unigramHandler.getFinalDictionary();
         final HashMap<String, ArrayList<WeightedString>> bigramMap = bigramHandler.getBigramMap();
@@ -280,7 +307,7 @@
      *
      * This method reads data from the parser and creates a new FusionDictionary with it.
      * The format parsed by this method is the format used before Ice Cream Sandwich,
-     * which has no support for bigrams or shortcuts.
+     * which has no support for bigrams or shortcuts/whitelist.
      * It is important to note that this method expects the parser to have already eaten
      * the first, all-encompassing tag.
      *
@@ -291,7 +318,7 @@
     /**
      * Writes a dictionary to an XML file.
      *
-     * The output format is the "second" format, which supports bigrams and shortcuts.
+     * The output format is the "second" format, which supports bigrams and shortcuts/whitelist.
      *
      * @param destination a destination stream to write to.
      * @param dict the dictionary to write.
diff --git a/tools/maketext/Android.mk b/tools/maketext/Android.mk
index 98731b7..77914ca 100644
--- a/tools/maketext/Android.mk
+++ b/tools/maketext/Android.mk
@@ -19,7 +19,6 @@
 LOCAL_SRC_FILES += $(call all-java-files-under,src)
 LOCAL_JAR_MANIFEST := etc/manifest.txt
 LOCAL_JAVA_RESOURCE_DIRS := res
-LOCAL_MODULE_TAGS := eng
 LOCAL_MODULE := maketext
 
 include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/tools/maketext/etc/Android.mk b/tools/maketext/etc/Android.mk
index 4fa194b..475676b 100644
--- a/tools/maketext/etc/Android.mk
+++ b/tools/maketext/etc/Android.mk
@@ -15,7 +15,6 @@
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
-LOCAL_MODULE_TAGS := eng
-
 LOCAL_PREBUILT_EXECUTABLES := maketext
+
 include $(BUILD_HOST_PREBUILT)
diff --git a/tools/maketext/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl b/tools/maketext/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
index f6c84ea..774094c 100644
--- a/tools/maketext/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
+++ b/tools/maketext/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 
 import java.util.HashMap;
@@ -45,14 +46,12 @@
  */
 public final class KeyboardTextsSet {
     // Language to texts map.
-    private static final HashMap<String, String[]> sLocaleToTextsMap =
-            new HashMap<String, String[]>();
-    private static final HashMap<String, Integer> sNameToIdsMap =
-            new HashMap<String, Integer>();
+    private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap();
+    private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
 
     private String[] mTexts;
     // Resource name to text map.
-    private HashMap<String, String> mResourceNameToTextsMap = new HashMap<String, String>();
+    private HashMap<String, String> mResourceNameToTextsMap = CollectionUtils.newHashMap();
 
     public void setLanguage(final String language) {
         mTexts = sLocaleToTextsMap.get(language);