resolved conflicts for merge of b1fbd696 to jb-mr1-dev bug:6809603

Change-Id: I23dcf45892d94db88f424a4ee0ae440a2c743d9b
diff --git a/dictionaries/en_gb_wordlist.xml b/dictionaries/en_gb_wordlist.xml
index c2af46e..09078ae 100644
--- a/dictionaries/en_gb_wordlist.xml
+++ b/dictionaries/en_gb_wordlist.xml
@@ -1,4 +1,4 @@
-<wordlist locale="en_GB" description="English (UK)" date="1340038724" version="16">
+<wordlist locale="en_GB" description="English (UK)" date="1340965760" version="17">
  <w f="222" flags="">the</w>
  <w f="214" flags="">of</w>
  <w f="212" flags="">and</w>
@@ -256,7 +256,6 @@
  <w f="155" flags="">national</w>
  <w f="155" flags="">nice</w>
  <w f="155" flags="">non</w>
- <w f="155" flags="">north</w>
  <w f="155" flags="">period</w>
  <w f="155" flags="">son</w>
  <w f="155" flags="">south</w>
@@ -283,7 +282,6 @@
  <w f="154" flags="">present</w>
  <w f="154" flags="">produced</w>
  <w f="154" flags="">record</w>
- <w f="154" flags="">role</w>
  <w f="154" flags="">six</w>
  <w f="154" flags="">species</w>
  <w f="154" flags="">started</w>
@@ -607,6 +605,7 @@
  <w f="145" flags="">move</w>
  <w f="145" flags="">natural</w>
  <w f="145" flags="">network</w>
+ <w f="145" flags="">north</w>
  <w f="145" flags="">northern</w>
  <w f="145" flags="">novel</w>
  <w f="145" flags="">numerous</w>
@@ -708,6 +707,7 @@
  <w f="144" flags="">rate</w>
  <w f="144" flags="">recent</w>
  <w f="144" flags="">remains</w>
+ <w f="144" flags="">role</w>
  <w f="144" flags="">seat</w>
  <w f="144" flags="">self</w>
  <w f="144" flags="">shown</w>
@@ -13520,7 +13520,6 @@
  <w f="97" flags="">Kay</w>
  <w f="97" flags="">Klein</w>
  <w f="97" flags="">Kuwait</w>
- <w f="97" flags="">Lang</w>
  <w f="97" flags="">Leigh</w>
  <w f="97" flags="">Leipzig</w>
  <w f="97" flags="">Lennon</w>
@@ -14169,7 +14168,6 @@
  <w f="96" flags="">MacLeod</w>
  <w f="96" flags="">Mafia</w>
  <w f="96" flags="">Maharashtra</w>
- <w f="96" flags="">Marina</w>
  <w f="96" flags="">McCall</w>
  <w f="96" flags="">McClellan</w>
  <w f="96" flags="">McGuire</w>
@@ -15902,7 +15900,6 @@
  <w f="94" flags="">leakage</w>
  <w f="94" flags="">leaks</w>
  <w f="94" flags="">lecturing</w>
- <w f="94" flags="">lesbians</w>
  <w f="94" flags="">licences</w>
  <w f="94" flags="">life's</w>
  <w f="94" flags="">lifeboat</w>
@@ -16780,7 +16777,6 @@
  <w f="93" flags="">sclerosis</w>
  <w f="93" flags="">scream</w>
  <w f="93" flags="">screenplays</w>
- <w f="93" flags="">screws</w>
  <w f="93" flags="">seaport</w>
  <w f="93" flags="">seawater</w>
  <w f="93" flags="">secretaries</w>
@@ -29553,7 +29549,6 @@
  <w f="79" flags="">Getty</w>
  <w f="79" flags="">Gleason</w>
  <w f="79" flags="">Godwin</w>
- <w f="79" flags="">Gotha</w>
  <w f="79" flags="">Grantham</w>
  <w f="79" flags="">Greenpeace</w>
  <w f="79" flags="">Grenoble</w>
@@ -32722,7 +32717,6 @@
  <w f="77" flags="">leeward</w>
  <w f="77" flags="">lengthwise</w>
  <w f="77" flags="">lentils</w>
- <w f="77" flags="">lesbianism</w>
  <w f="77" flags="">leveraging</w>
  <w f="77" flags="">lib</w>
  <w f="77" flags="">libertarianism</w>
@@ -49865,7 +49859,6 @@
  <w f="64" flags="abbreviation">ECW's</w>
  <w f="64" flags="abbreviation">EEOC</w>
  <w f="64" flags="abbreviation">EKG</w>
- <w f="64" flags="abbreviation">ENS</w>
  <w f="64" flags="abbreviation">ESOL</w>
  <w f="64" flags="">Edgeworth</w>
  <w f="64" flags="">Edirne</w>
@@ -54575,7 +54568,6 @@
  <w f="61" flags="">Hersey</w>
  <w f="61" flags="">Hiatt</w>
  <w f="61" flags="">Himmler's</w>
- <w f="61" flags="">Hoke</w>
  <w f="61" flags="">Hormuz</w>
  <w f="61" flags="">Hosea</w>
  <w f="61" flags="">Hubli</w>
@@ -89915,7 +89907,6 @@
  <w f="38" flags="">Hally</w>
  <w f="38" flags="">Harpo's</w>
  <w f="38" flags="">Higbee's</w>
- <w f="38" flags="">Hoke's</w>
  <w f="38" flags="">Honeywell's</w>
  <w f="38" flags="">Horatian</w>
  <w f="38" flags="">Iago's</w>
@@ -109454,7 +109445,6 @@
  <w f="25" flags="">leets</w>
  <w f="25" flags="">legitimism</w>
  <w f="25" flags="">leopardskin</w>
- <w f="25" flags="">lesbian's</w>
  <w f="25" flags="">lessee's</w>
  <w f="25" flags="">lethargically</w>
  <w f="25" flags="">libber</w>
@@ -112568,7 +112558,6 @@
  <w f="23" flags="">pickerels</w>
  <w f="23" flags="">piecers</w>
  <w f="23" flags="">pigmenting</w>
- <w f="23" flags="">pill's</w>
  <w f="23" flags="">pilled</w>
  <w f="23" flags="">pillorying</w>
  <w f="23" flags="">pinked</w>
@@ -117332,7 +117321,6 @@
  <w f="18" flags="">Hinsdale's</w>
  <w f="18" flags="">Hispaniola's</w>
  <w f="18" flags="">Hohenzollern's</w>
- <w f="18" flags="">Hokes</w>
  <w f="18" flags="">Horst's</w>
  <w f="18" flags="">Housecat's</w>
  <w f="18" flags="">Hughie's</w>
@@ -155568,6 +155556,7 @@
  <w f="0" flags="">XP</w>
  <w f="0" flags="offensive">Yoni's</w>
  <w f="0" flags="">acct</w>
+ <w f="0">admin</w>
  <w f="0" flags="n">adult</w>
  <w f="0" flags="medical">adulteress</w>
  <w f="0" flags="medical">adulteresses</w>
@@ -156092,6 +156081,9 @@
  <w f="0" flags="">lei</w>
  <w f="0" flags="">lem</w>
  <w f="0" flags="n">lesbian</w>
+ <w f="0" flags="">lesbian's</w>
+ <w f="0" flags="">lesbianism</w>
+ <w f="0" flags="">lesbians</w>
  <w f="0" flags="offensive">letch</w>
  <w f="0" flags="n">libido</w>
  <w f="0" flags="s">librium</w>
@@ -156267,6 +156259,7 @@
  <w f="0" flags="medical">phosphaturia</w>
  <w f="0" flags="medical">phosphaturic</w>
  <w f="0" flags="s">pill</w>
+ <w f="0" flags="">pill's</w>
  <w f="0" flags="s">pills</w>
  <w f="0" flags="offensive">pimp</w>
  <w f="0" flags="offensive">pimp's</w>
@@ -156428,6 +156421,7 @@
  <w f="0" flags="r">screw</w>
  <w f="0" flags="n">screwed</w>
  <w f="0" flags="n">screwing</w>
+ <w f="0" flags="">screws</w>
  <w f="0" flags="medical">scrota</w>
  <w f="0" flags="medical">scrotal</w>
  <w f="0" flags="medical">scrotum</w>
@@ -156641,6 +156635,7 @@
  <w f="0" flags="babytalk">twat</w>
  <w f="0" flags="babytalk">twats</w>
  <w f="0" flags="">twit</w>
+ <w f="0">ui</w>
  <w f="0">ull</w>
  <w f="0" flags="babytalk">underclothing</w>
  <w f="0" flags="babytalk">underwear</w>
@@ -156747,6 +156742,7 @@
  <w f="0" flags="medical">uterus</w>
  <w f="0" flags="medical">uterus's</w>
  <w f="0" flags="medical">uteruses</w>
+ <w f="0">ux</w>
  <w f="0" flags="medical">vagina</w>
  <w f="0" flags="medical">vagina's</w>
  <w f="0" flags="medical">vaginae</w>
diff --git a/dictionaries/en_us_wordlist.xml b/dictionaries/en_us_wordlist.xml
index 3cafbd7..0b158f5 100644
--- a/dictionaries/en_us_wordlist.xml
+++ b/dictionaries/en_us_wordlist.xml
@@ -1,4 +1,4 @@
-<wordlist locale="en_US" description="English (US)" date="1340038693" version="16">
+<wordlist locale="en_US" description="English (US)" date="1340965726" version="17">
  <w f="222" flags="">the</w>
  <w f="214" flags="">of</w>
  <w f="212" flags="">and</w>
@@ -255,7 +255,6 @@
  <w f="155" flags="">national</w>
  <w f="155" flags="">nice</w>
  <w f="155" flags="">non</w>
- <w f="155" flags="">north</w>
  <w f="155" flags="">period</w>
  <w f="155" flags="">son</w>
  <w f="155" flags="">south</w>
@@ -282,7 +281,6 @@
  <w f="154" flags="">present</w>
  <w f="154" flags="">produced</w>
  <w f="154" flags="">record</w>
- <w f="154" flags="">role</w>
  <w f="154" flags="">six</w>
  <w f="154" flags="">species</w>
  <w f="154" flags="">started</w>
@@ -587,7 +585,6 @@
  <w f="145" flags="">July</w>
  <w f="145" flags="">June</w>
  <w f="145" flags="">March</w>
- <w f="145" flags="">North</w>
  <w f="145" flags="">October</w>
  <w f="145" flags="">active</w>
  <w f="145" flags="">always</w>
@@ -614,6 +611,7 @@
  <w f="145" flags="">move</w>
  <w f="145" flags="">natural</w>
  <w f="145" flags="">network</w>
+ <w f="145" flags="">north</w>
  <w f="145" flags="">northern</w>
  <w f="145" flags="">novel</w>
  <w f="145" flags="">numerous</w>
@@ -717,6 +715,7 @@
  <w f="144" flags="">rate</w>
  <w f="144" flags="">recent</w>
  <w f="144" flags="">remains</w>
+ <w f="144" flags="">role</w>
  <w f="144" flags="">seat</w>
  <w f="144" flags="">self</w>
  <w f="144" flags="">shown</w>
@@ -12069,6 +12068,7 @@
  <w f="100" flags="">Nash</w>
  <w f="100" flags="">Newark</w>
  <w f="100" flags="">Norse</w>
+ <w f="100" flags="">North</w>
  <w f="100" flags="">Norton</w>
  <w f="100" flags="">Norwich</w>
  <w f="100" flags="">Okinawa</w>
@@ -13996,7 +13996,6 @@
  <w f="97" flags="">Klein</w>
  <w f="97" flags="">Kuwait</w>
  <w f="97" flags="">Ladies</w>
- <w f="97" flags="">Lang</w>
  <w f="97" flags="">Leigh</w>
  <w f="97" flags="">Leipzig</w>
  <w f="97" flags="">Lennon</w>
@@ -14677,7 +14676,6 @@
  <w f="96" flags="">Madras</w>
  <w f="96" flags="">Mafia</w>
  <w f="96" flags="">Maharashtra</w>
- <w f="96" flags="">Marina</w>
  <w f="96" flags="">Master's</w>
  <w f="96" flags="">McCall</w>
  <w f="96" flags="">McClellan</w>
@@ -16141,7 +16139,6 @@
  <w f="94" flags="">Stratford</w>
  <w f="94" flags="">Strauss</w>
  <w f="94" flags="">Stuttgart</w>
- <w f="94" flags="">Sunshine</w>
  <w f="94" flags="">Suzuki</w>
  <w f="94" flags="">Taiwanese</w>
  <w f="94" flags="">Talbot</w>
@@ -16154,7 +16151,6 @@
  <w f="94" flags="">Tyne</w>
  <w f="94" flags="abbreviation">UNC</w>
  <w f="94" flags="abbreviation">UNICEF</w>
- <w f="94" flags="">UNIX</w>
  <w f="94" flags="abbreviation">USDA</w>
  <w f="94" flags="">Unix</w>
  <w f="94" flags="">Valentine</w>
@@ -16488,7 +16484,6 @@
  <w f="94" flags="">leakage</w>
  <w f="94" flags="">leaks</w>
  <w f="94" flags="">lecturing</w>
- <w f="94" flags="">lesbians</w>
  <w f="94" flags="">leveled</w>
  <w f="94" flags="">life's</w>
  <w f="94" flags="">lifeboat</w>
@@ -17402,7 +17397,6 @@
  <w f="93" flags="">sclerosis</w>
  <w f="93" flags="">scream</w>
  <w f="93" flags="">screenplays</w>
- <w f="93" flags="">screws</w>
  <w f="93" flags="">seaport</w>
  <w f="93" flags="">seawater</w>
  <w f="93" flags="">secretaries</w>
@@ -30682,7 +30676,6 @@
  <w f="79" flags="">Ghats</w>
  <w f="79" flags="">Gleason</w>
  <w f="79" flags="">Godwin</w>
- <w f="79" flags="">Gotha</w>
  <w f="79" flags="">Grantham</w>
  <w f="79" flags="">Greenpeace</w>
  <w f="79" flags="">Grenoble</w>
@@ -33954,7 +33947,6 @@
  <w f="77" flags="">leeward</w>
  <w f="77" flags="">lengthwise</w>
  <w f="77" flags="">lentils</w>
- <w f="77" flags="">lesbianism</w>
  <w f="77" flags="">leveraging</w>
  <w f="77" flags="">lib</w>
  <w f="77" flags="">libertarianism</w>
@@ -92910,7 +92902,6 @@
  <w f="38" flags="">Harpo's</w>
  <w f="38" flags="">Higbee's</w>
  <w f="38">Hipparchus</w>
- <w f="38" flags="">Hoke's</w>
  <w f="38" flags="">Honeywell's</w>
  <w f="38" flags="">Horatian</w>
  <w f="38" flags="">Iago's</w>
@@ -112796,7 +112787,6 @@
  <w f="25" flags="">leets</w>
  <w f="25" flags="">legitimism</w>
  <w f="25" flags="">leopardskin</w>
- <w f="25" flags="">lesbian's</w>
  <w f="25" flags="">lessee's</w>
  <w f="25" flags="">lethargically</w>
  <w f="25" flags="">libber</w>
@@ -115952,7 +115942,6 @@
  <w f="23" flags="">pickerels</w>
  <w f="23" flags="">piecers</w>
  <w f="23" flags="">pigmenting</w>
- <w f="23" flags="">pill's</w>
  <w f="23" flags="">pilled</w>
  <w f="23" flags="">pillorying</w>
  <w f="23" flags="">pinked</w>
@@ -120765,7 +120754,6 @@
  <w f="18" flags="">Hinsdale's</w>
  <w f="18" flags="">Hispaniola's</w>
  <w f="18" flags="">Hohenzollern's</w>
- <w f="18" flags="">Hokes</w>
  <w f="18" flags="">Horst's</w>
  <w f="18" flags="">Housecat's</w>
  <w f="18" flags="">Hughie's</w>
@@ -159318,6 +159306,7 @@
  <w f="0" flags="">XP</w>
  <w f="0" flags="offensive">Yoni's</w>
  <w f="0" flags="">acct</w>
+ <w f="0">admin</w>
  <w f="0" flags="n">adult</w>
  <w f="0" flags="medical">adulteress</w>
  <w f="0" flags="medical">adulteresses</w>
@@ -159849,6 +159838,9 @@
  <w f="0" flags="medical">leching</w>
  <w f="0" flags="">lei</w>
  <w f="0" flags="n">lesbian</w>
+ <w f="0" flags="">lesbian's</w>
+ <w f="0" flags="">lesbianism</w>
+ <w f="0" flags="">lesbians</w>
  <w f="0" flags="offensive">letch</w>
  <w f="0" flags="n">libido</w>
  <w f="0" flags="n">lick</w>
@@ -160024,6 +160016,7 @@
  <w f="0" flags="medical">phosphaturia</w>
  <w f="0" flags="medical">phosphaturic</w>
  <w f="0" flags="s">pill</w>
+ <w f="0" flags="">pill's</w>
  <w f="0" flags="s">pills</w>
  <w f="0" flags="offensive">pimp</w>
  <w f="0" flags="offensive">pimp's</w>
@@ -160183,6 +160176,7 @@
  <w f="0" flags="r">screw</w>
  <w f="0" flags="n">screwed</w>
  <w f="0" flags="n">screwing</w>
+ <w f="0" flags="">screws</w>
  <w f="0" flags="medical">scrota</w>
  <w f="0" flags="medical">scrotal</w>
  <w f="0" flags="medical">scrotum</w>
@@ -160398,6 +160392,7 @@
  <w f="0" flags="babytalk">twat</w>
  <w f="0" flags="babytalk">twats</w>
  <w f="0" flags="">twit</w>
+ <w f="0">ui</w>
  <w f="0">ull</w>
  <w f="0" flags="">ump</w>
  <w f="0" flags="babytalk">underclothing</w>
@@ -160505,6 +160500,7 @@
  <w f="0" flags="medical">uterus</w>
  <w f="0" flags="medical">uterus's</w>
  <w f="0" flags="medical">uteruses</w>
+ <w f="0">ux</w>
  <w f="0" flags="medical">vagina</w>
  <w f="0" flags="medical">vagina's</w>
  <w f="0" flags="medical">vaginae</w>
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/dictionaries/en_wordlist.xml b/dictionaries/en_wordlist.xml
index 6f594d5..c6b3bdc 100644
--- a/dictionaries/en_wordlist.xml
+++ b/dictionaries/en_wordlist.xml
@@ -1,4 +1,4 @@
-<wordlist locale="en" description="English" date="1340038727" version="16">
+<wordlist locale="en" description="English" date="1340965763" version="17">
  <w f="222" flags="">the</w>
  <w f="214" flags="">of</w>
  <w f="212" flags="">and</w>
@@ -255,7 +255,6 @@
  <w f="155" flags="">national</w>
  <w f="155" flags="">nice</w>
  <w f="155" flags="">non</w>
- <w f="155" flags="">north</w>
  <w f="155" flags="">period</w>
  <w f="155" flags="">son</w>
  <w f="155" flags="">south</w>
@@ -282,7 +281,6 @@
  <w f="154" flags="">present</w>
  <w f="154" flags="">produced</w>
  <w f="154" flags="">record</w>
- <w f="154" flags="">role</w>
  <w f="154" flags="">six</w>
  <w f="154" flags="">species</w>
  <w f="154" flags="">started</w>
@@ -587,7 +585,6 @@
  <w f="145" flags="">July</w>
  <w f="145" flags="">June</w>
  <w f="145" flags="">March</w>
- <w f="145" flags="">North</w>
  <w f="145" flags="">October</w>
  <w f="145" flags="">active</w>
  <w f="145" flags="">always</w>
@@ -614,6 +611,7 @@
  <w f="145" flags="">move</w>
  <w f="145" flags="">natural</w>
  <w f="145" flags="">network</w>
+ <w f="145" flags="">north</w>
  <w f="145" flags="">northern</w>
  <w f="145" flags="">novel</w>
  <w f="145" flags="">numerous</w>
@@ -718,6 +716,7 @@
  <w f="144" flags="">rate</w>
  <w f="144" flags="">recent</w>
  <w f="144" flags="">remains</w>
+ <w f="144" flags="">role</w>
  <w f="144" flags="">seat</w>
  <w f="144" flags="">self</w>
  <w f="144" flags="">shown</w>
@@ -12162,6 +12161,7 @@
  <w f="100" flags="">Nash</w>
  <w f="100" flags="">Newark</w>
  <w f="100" flags="">Norse</w>
+ <w f="100" flags="">North</w>
  <w f="100" flags="">Norton</w>
  <w f="100" flags="">Norwich</w>
  <w f="100" flags="">Okinawa</w>
@@ -14103,7 +14103,6 @@
  <w f="97" flags="">Klein</w>
  <w f="97" flags="">Kuwait</w>
  <w f="97" flags="">Ladies</w>
- <w f="97" flags="">Lang</w>
  <w f="97" flags="">Leigh</w>
  <w f="97" flags="">Leipzig</w>
  <w f="97" flags="">Lennon</w>
@@ -14792,7 +14791,6 @@
  <w f="96" flags="">Madras</w>
  <w f="96" flags="">Mafia</w>
  <w f="96" flags="">Maharashtra</w>
- <w f="96" flags="">Marina</w>
  <w f="96" flags="">Master's</w>
  <w f="96" flags="">McCall</w>
  <w f="96" flags="">McClellan</w>
@@ -16273,7 +16271,6 @@
  <w f="94" flags="">Stratford</w>
  <w f="94" flags="">Strauss</w>
  <w f="94" flags="">Stuttgart</w>
- <w f="94" flags="">Sunshine</w>
  <w f="94" flags="">Suzuki</w>
  <w f="94" flags="abbreviation">TBS</w>
  <w f="94" flags="">Taiwanese</w>
@@ -16287,7 +16284,6 @@
  <w f="94" flags="">Tyne</w>
  <w f="94" flags="abbreviation">UNC</w>
  <w f="94" flags="abbreviation">UNICEF</w>
- <w f="94" flags="">UNIX</w>
  <w f="94" flags="abbreviation">USDA</w>
  <w f="94" flags="">Unix</w>
  <w f="94" flags="">Valentine</w>
@@ -16624,7 +16620,6 @@
  <w f="94" flags="">leakage</w>
  <w f="94" flags="">leaks</w>
  <w f="94" flags="">lecturing</w>
- <w f="94" flags="">lesbians</w>
  <w f="94" flags="">leveled</w>
  <w f="94" flags="">licences</w>
  <w f="94" flags="">life's</w>
@@ -17555,7 +17550,6 @@
  <w f="93" flags="">sclerosis</w>
  <w f="93" flags="">scream</w>
  <w f="93" flags="">screenplays</w>
- <w f="93" flags="">screws</w>
  <w f="93" flags="">seaport</w>
  <w f="93" flags="">seawater</w>
  <w f="93" flags="">secretaries</w>
@@ -31073,7 +31067,6 @@
  <w f="79" flags="">Ghats</w>
  <w f="79" flags="">Gleason</w>
  <w f="79" flags="">Godwin</w>
- <w f="79" flags="">Gotha</w>
  <w f="79" flags="">Grantham</w>
  <w f="79" flags="">Greenpeace</w>
  <w f="79" flags="">Grenoble</w>
@@ -34402,7 +34395,6 @@
  <w f="77" flags="">leeward</w>
  <w f="77" flags="">lengthwise</w>
  <w f="77" flags="">lentils</w>
- <w f="77" flags="">lesbianism</w>
  <w f="77" flags="">leveraging</w>
  <w f="77" flags="">lib</w>
  <w f="77" flags="">libertarianism</w>
@@ -52519,7 +52511,6 @@
  <w f="64" flags="abbreviation">ECW's</w>
  <w f="64" flags="abbreviation">EEOC</w>
  <w f="64" flags="abbreviation">EKG</w>
- <w f="64" flags="abbreviation">ENS</w>
  <w f="64" flags="abbreviation">ESOL</w>
  <w f="64" flags="">Edgeworth</w>
  <w f="64" flags="">Edirne</w>
@@ -57486,7 +57477,6 @@
  <w f="61" flags="">Hersey</w>
  <w f="61" flags="">Hiatt</w>
  <w f="61" flags="">Himmler's</w>
- <w f="61" flags="">Hoke</w>
  <w f="61" flags="">Hormuz</w>
  <w f="61" flags="">Hosea</w>
  <w f="61" flags="">Hubli</w>
@@ -94993,7 +94983,6 @@
  <w f="38" flags="">Harpo's</w>
  <w f="38" flags="">Higbee's</w>
  <w f="38">Hipparchus</w>
- <w f="38" flags="">Hoke's</w>
  <w f="38" flags="">Honeywell's</w>
  <w f="38" flags="">Horatian</w>
  <w f="38" flags="">Iago's</w>
@@ -115756,7 +115745,6 @@
  <w f="25" flags="">leets</w>
  <w f="25" flags="">legitimism</w>
  <w f="25" flags="">leopardskin</w>
- <w f="25" flags="">lesbian's</w>
  <w f="25" flags="">lessee's</w>
  <w f="25" flags="">lethargically</w>
  <w f="25" flags="">libber</w>
@@ -119045,7 +119033,6 @@
  <w f="23" flags="">pickerels</w>
  <w f="23" flags="">piecers</w>
  <w f="23" flags="">pigmenting</w>
- <w f="23" flags="">pill's</w>
  <w f="23" flags="">pilled</w>
  <w f="23" flags="">pillorying</w>
  <w f="23" flags="">pinked</w>
@@ -124070,7 +124057,6 @@
  <w f="18" flags="">Hinsdale's</w>
  <w f="18" flags="">Hispaniola's</w>
  <w f="18" flags="">Hohenzollern's</w>
- <w f="18" flags="">Hokes</w>
  <w f="18" flags="">Horst's</w>
  <w f="18" flags="">Housecat's</w>
  <w f="18" flags="">Hughie's</w>
@@ -164106,6 +164092,7 @@
  <w f="0" flags="">XP</w>
  <w f="0" flags="offensive">Yoni's</w>
  <w f="0" flags="">acct</w>
+ <w f="0">admin</w>
  <w f="0" flags="n">adult</w>
  <w f="0" flags="medical">adulteress</w>
  <w f="0" flags="medical">adulteresses</w>
@@ -164656,6 +164643,9 @@
  <w f="0" flags="">lei</w>
  <w f="0" flags="">lem</w>
  <w f="0" flags="n">lesbian</w>
+ <w f="0" flags="">lesbian's</w>
+ <w f="0" flags="">lesbianism</w>
+ <w f="0" flags="">lesbians</w>
  <w f="0" flags="offensive">letch</w>
  <w f="0" flags="n">libido</w>
  <w f="0" flags="s">librium</w>
@@ -164842,6 +164832,7 @@
  <w f="0" flags="medical">phosphaturia</w>
  <w f="0" flags="medical">phosphaturic</w>
  <w f="0" flags="s">pill</w>
+ <w f="0" flags="">pill's</w>
  <w f="0" flags="s">pills</w>
  <w f="0" flags="offensive">pimp</w>
  <w f="0" flags="offensive">pimp's</w>
@@ -165004,6 +164995,7 @@
  <w f="0" flags="r">screw</w>
  <w f="0" flags="n">screwed</w>
  <w f="0" flags="n">screwing</w>
+ <w f="0" flags="">screws</w>
  <w f="0" flags="medical">scrota</w>
  <w f="0" flags="medical">scrotal</w>
  <w f="0" flags="medical">scrotum</w>
@@ -165226,6 +165218,7 @@
  <w f="0" flags="babytalk">twat</w>
  <w f="0" flags="babytalk">twats</w>
  <w f="0" flags="">twit</w>
+ <w f="0">ui</w>
  <w f="0">ull</w>
  <w f="0" flags="">ump</w>
  <w f="0" flags="babytalk">underclothing</w>
@@ -165335,6 +165328,7 @@
  <w f="0" flags="medical">uterus</w>
  <w f="0" flags="medical">uterus's</w>
  <w f="0" flags="medical">uteruses</w>
+ <w f="0">ux</w>
  <w f="0" flags="medical">vagina</w>
  <w f="0" flags="medical">vagina's</w>
  <w f="0" flags="medical">vaginae</w>
diff --git a/dictionaries/fr_wordlist.xml b/dictionaries/fr_wordlist.xml
index 3990988..6053f48 100644
--- a/dictionaries/fr_wordlist.xml
+++ b/dictionaries/fr_wordlist.xml
@@ -1,4 +1,4 @@
-<wordlist locale="fr" description="Français" date="1339787661" version="15" options="french_ligature_processing">
+<wordlist locale="fr" description="Français" date="1340965148" version="17" options="french_ligature_processing">
  <w f="209" flags="">de</w>
  <w f="200" flags="">la</w>
  <w f="197" flags="">et</w>
@@ -1069,6 +1069,7 @@
  <w f="125" flags="">finit</w>
  <w f="125" flags="">fleuve</w>
  <w f="125" flags="">fondateur</w>
+ <w f="125">généralement</w>
  <w f="125" flags="">hors</w>
  <w f="125" flags="">ici</w>
  <w f="125" flags="">importantes</w>
@@ -13168,7 +13169,6 @@
  <w f="90" flags="">play-offs</w>
  <w f="90" flags="">plaça</w>
  <w f="90" flags="">plonger</w>
- <w f="90" flags="">pole</w>
  <w f="90" flags="">polonaises</w>
  <w f="90" flags="">polymères</w>
  <w f="90" flags="">popularisé</w>
@@ -15098,7 +15098,6 @@
  <w f="87" flags="">donneur</w>
  <w f="87" flags="">dorés</w>
  <w f="87" flags="">doucement</w>
- <w f="87" flags="">doyenné</w>
  <w f="87" flags="">drames</w>
  <w f="87" flags="">dualité</w>
  <w f="87" flags="">ducale</w>
@@ -20845,7 +20844,6 @@
  <w f="81" flags="">matérialisme</w>
  <w f="81" flags="">mauve</w>
  <w f="81" flags="">maximiser</w>
- <w f="81" flags="">media</w>
  <w f="81" flags="">melon</w>
  <w f="81" flags="">mendiants</w>
  <w f="81" flags="">menuiserie</w>
@@ -21702,7 +21700,6 @@
  <w f="80" flags="">gallo-romains</w>
  <w f="80" flags="">galop</w>
  <w f="80" flags="">gangster</w>
- <w f="80" flags="">general</w>
  <w f="80" flags="">gentilé</w>
  <w f="80" flags="">gewurztraminer</w>
  <w f="80" flags="">gisant</w>
@@ -24213,7 +24210,6 @@
  <w f="78" flags="">mythologies</w>
  <w f="78" flags="">mâchicoulis</w>
  <w f="78" flags="">méconnue</w>
- <w f="78" flags="">médina</w>
  <w f="78" flags="">mésopotamienne</w>
  <w f="78" flags="">métriques</w>
  <w f="78" flags="">n'apprécie</w>
@@ -26343,7 +26339,6 @@
  <w f="76" flags="">graphème</w>
  <w f="76" flags="">gravitationnelles</w>
  <w f="76" flags="">greffer</w>
- <w f="76" flags="">guérilleros</w>
  <w f="76" flags="">gèle</w>
  <w f="76" flags="">génitales</w>
  <w f="76" flags="">généalogiques</w>
@@ -31797,7 +31792,6 @@
  <w f="72" flags="">gouvernait</w>
  <w f="72" flags="">gouvernant</w>
  <w f="72" flags="">goéland</w>
- <w f="72" flags="">grace</w>
  <w f="72" flags="">groove</w>
  <w f="72" flags="">gymnastes</w>
  <w f="72" flags="">génomes</w>
@@ -32178,7 +32172,6 @@
  <w f="72" flags="">polders</w>
  <w f="72" flags="">poliomyélite</w>
  <w f="72" flags="">polychromie</w>
- <w f="72" flags="">polynomiale</w>
  <w f="72" flags="">polyvalents</w>
  <w f="72" flags="">pondus</w>
  <w f="72" flags="">pool</w>
@@ -33103,7 +33096,6 @@
  <w f="71" flags="">d'Austin</w>
  <w f="71" flags="">d'Avalon</w>
  <w f="71" flags="">d'Azov</w>
- <w f="71" flags="">d'Aïn</w>
  <w f="71" flags="">d'Emily</w>
  <w f="71" flags="">d'Esther</w>
  <w f="71" flags="">d'Halicarnasse</w>
@@ -33402,7 +33394,6 @@
  <w f="71" flags="">imaginés</w>
  <w f="71" flags="">immigrant</w>
  <w f="71" flags="">imparfait</w>
- <w f="71" flags="">imperium</w>
  <w f="71" flags="">implicites</w>
  <w f="71" flags="">importateur</w>
  <w f="71" flags="">imposable</w>
@@ -33808,7 +33799,6 @@
  <w f="71" flags="">rasées</w>
  <w f="71" flags="">rationaliser</w>
  <w f="71" flags="">rayés</w>
- <w f="71" flags="">real</w>
  <w f="71" flags="">rebaptiser</w>
  <w f="71" flags="">rebâti</w>
  <w f="71" flags="">recensant</w>
@@ -34659,7 +34649,6 @@
  <w f="70" flags="">cénotaphe</w>
  <w f="70" flags="">d'Achaïe</w>
  <w f="70" flags="">d'Agrippa</w>
- <w f="70" flags="">d'Al-Qaida</w>
  <w f="70" flags="">d'Alabama</w>
  <w f="70" flags="">d'Albon</w>
  <w f="70" flags="">d'Alessandro</w>
@@ -34674,7 +34663,6 @@
  <w f="70" flags="">d'Argos</w>
  <w f="70" flags="">d'Assouan</w>
  <w f="70" flags="">d'Avaugour</w>
- <w f="70" flags="">d'Avila</w>
  <w f="70" flags="">d'Azincourt</w>
  <w f="70" flags="">d'East</w>
  <w f="70" flags="">d'Edmund</w>
@@ -34788,7 +34776,6 @@
  <w f="70" flags="">diva</w>
  <w f="70" flags="">diverge</w>
  <w f="70" flags="">divergents</w>
- <w f="70" flags="">djebel</w>
  <w f="70" flags="">dominical</w>
  <w f="70" flags="">dopamine</w>
  <w f="70" flags="">dopé</w>
@@ -36252,7 +36239,6 @@
  <w f="69" flags="">concevant</w>
  <w f="69" flags="">concurrencée</w>
  <w f="69" flags="">condense</w>
- <w f="69" flags="">condottiere</w>
  <w f="69" flags="">confidente</w>
  <w f="69" flags="">confine</w>
  <w f="69" flags="">conjonctif</w>
@@ -36274,7 +36260,6 @@
  <w f="69" flags="">coqueluche</w>
  <w f="69" flags="">corda</w>
  <w f="69" flags="">cordiales</w>
- <w f="69" flags="">core</w>
  <w f="69" flags="">corpulence</w>
  <w f="69" flags="">corrosif</w>
  <w f="69" flags="">cottage</w>
@@ -40119,7 +40104,6 @@
  <w f="67" flags="">l'Estrie</w>
  <w f="67" flags="abbreviation">l'IFK</w>
  <w f="67" flags="abbreviation">l'INSEP</w>
- <w f="67" flags="">l'Imperial</w>
  <w f="67" flags="">l'Indien</w>
  <w f="67" flags="">l'Istrie</w>
  <w f="67" flags="abbreviation">l'OSCE</w>
@@ -40246,7 +40230,6 @@
  <w f="67" flags="">mayonnaise</w>
  <w f="67" flags="">maîtrisait</w>
  <w f="67" flags="">maîtrises</w>
- <w f="67" flags="">medium</w>
  <w f="67" flags="">melons</w>
  <w f="67" flags="">membranaires</w>
  <w f="67" flags="">menottes</w>
@@ -41573,7 +41556,6 @@
  <w f="66" flags="">d'Auxonne</w>
  <w f="66" flags="">d'Avellino</w>
  <w f="66" flags="">d'Eddy</w>
- <w f="66" flags="">d'Eden</w>
  <w f="66" flags="">d'Erich</w>
  <w f="66" flags="">d'Eschyle</w>
  <w f="66" flags="">d'Essen</w>
@@ -42181,7 +42163,6 @@
  <w f="66" flags="">mazout</w>
  <w f="66" flags="">maçonné</w>
  <w f="66" flags="">mec</w>
- <w f="66" flags="">medias</w>
  <w f="66" flags="">menaçants</w>
  <w f="66" flags="">mensurations</w>
  <w f="66" flags="">merchandising</w>
@@ -42343,7 +42324,6 @@
  <w f="66" flags="">plénipotentiaires</w>
  <w f="66" flags="">pléthore</w>
  <w f="66" flags="">poignarder</w>
- <w f="66" flags="">polynomial</w>
  <w f="66" flags="">polynésiens</w>
  <w f="66" flags="">pont-canal</w>
  <w f="66" flags="">popularisa</w>
@@ -44095,7 +44075,6 @@
  <w f="65" flags="">manipulent</w>
  <w f="65" flags="">manquèrent</w>
  <w f="65" flags="">manqués</w>
- <w f="65" flags="">maqam</w>
  <w f="65" flags="">marchaient</w>
  <w f="65" flags="">marginalisé</w>
  <w f="65" flags="">marre</w>
@@ -44231,7 +44210,6 @@
  <w f="65" flags="">pensais</w>
  <w f="65" flags="">pensante</w>
  <w f="65" flags="">perdrait</w>
- <w f="65" flags="">perestroïka</w>
  <w f="65" flags="">perfide</w>
  <w f="65" flags="">perméables</w>
  <w f="65" flags="">perpétuant</w>
@@ -45576,7 +45554,6 @@
  <w f="64" flags="">dormance</w>
  <w f="64" flags="">dormi</w>
  <w f="64" flags="">doublent</w>
- <w f="64" flags="">doyennés</w>
  <w f="64" flags="">dragueur</w>
  <w f="64" flags="">dragueurs</w>
  <w f="64" flags="">drainées</w>
@@ -45803,7 +45780,6 @@
  <w f="64" flags="">herbicide</w>
  <w f="64" flags="">heurtèrent</w>
  <w f="64" flags="">hiboux</w>
- <w f="64" flags="">hindî</w>
  <w f="64" flags="">hiragana</w>
  <w f="64" flags="">hivernaux</w>
  <w f="64" flags="">hivernent</w>
@@ -46826,6 +46802,7 @@
  <w f="63" flags="">Dyck</w>
  <w f="63" flags="">Délos</w>
  <w f="63" flags="">Eaton</w>
+ <w f="63" flags="">El-Kébir</w>
  <w f="63" flags="">Elgar</w>
  <w f="63" flags="">Emmanuel-Philibert</w>
  <w f="63" flags="">Erasmus</w>
@@ -47645,7 +47622,6 @@
  <w f="63" flags="">déterrer</w>
  <w f="63" flags="">efficiente</w>
  <w f="63" flags="">effroi</w>
- <w f="63" flags="">el-Kébir</w>
  <w f="63" flags="">embarcadère</w>
  <w f="63" flags="">embarquement</w>
  <w f="63" flags="">embusqués</w>
@@ -50498,7 +50474,6 @@
  <w f="62" flags="">pointures</w>
  <w f="62" flags="">politicienne</w>
  <w f="62" flags="">polyarthrite</w>
- <w f="62" flags="">polynomiales</w>
  <w f="62" flags="">polythéiste</w>
  <w f="62" flags="">ponctue</w>
  <w f="62" flags="">pondu</w>
@@ -52102,7 +52077,6 @@
  <w f="61" flags="">fécondée</w>
  <w f="61" flags="">fétichistes</w>
  <w f="61" flags="">fêta</w>
- <w f="61" flags="">galle</w>
  <w f="61" flags="">gallons</w>
  <w f="61" flags="">gamelan</w>
  <w f="61" flags="">gantois</w>
@@ -52406,7 +52380,6 @@
  <w f="61" flags="">lycanthropes</w>
  <w f="61" flags="">lymphe</w>
  <w f="61" flags="">lyrics</w>
- <w f="61" flags="">länder</w>
  <w f="61" flags="">m'appelle</w>
  <w f="61" flags="">madriers</w>
  <w f="61" flags="">mafieuses</w>
@@ -52462,7 +52435,6 @@
  <w f="61" flags="">modifiables</w>
  <w f="61" flags="">monade</w>
  <w f="61" flags="">monophysisme</w>
- <w f="61" flags="">mons</w>
  <w f="61" flags="">montgolfières</w>
  <w f="61" flags="">montreront</w>
  <w f="61" flags="">monégasques</w>
@@ -53890,7 +53862,6 @@
  <w f="60" flags="">couteuse</w>
  <w f="60" flags="">craignit</w>
  <w f="60" flags="">cramoisi</w>
- <w f="60" flags="">crane</w>
  <w f="60" flags="">cravates</w>
  <w f="60" flags="">cristallisée</w>
  <w f="60" flags="">crochu</w>
@@ -54259,7 +54230,6 @@
  <w f="60" flags="">gazettes</w>
  <w f="60" flags="">gaziers</w>
  <w f="60" flags="">gazéification</w>
- <w f="60" flags="">gaëlique</w>
  <w f="60" flags="">geai</w>
  <w f="60" flags="">gestions</w>
  <w f="60" flags="">glissée</w>
@@ -54422,7 +54392,6 @@
  <w f="60" flags="">l'Argens</w>
  <w f="60" flags="">l'Audiencia</w>
  <w f="60" flags="">l'Aéro-Club</w>
- <w f="60" flags="">l'Eden</w>
  <w f="60" flags="">l'Est-Anglie</w>
  <w f="60" flags="">l'Huveaune</w>
  <w f="60" flags="">l'Ibar</w>
@@ -54448,7 +54417,6 @@
  <w f="60" flags="">l'apposition</w>
  <w f="60" flags="">l'aristocrate</w>
  <w f="60" flags="">l'arrangeur</w>
- <w f="60" flags="">l'artefact</w>
  <w f="60" flags="">l'assignation</w>
  <w f="60" flags="">l'associer</w>
  <w f="60" flags="">l'attaquent</w>
@@ -54902,6 +54870,7 @@
  <w f="60" flags="">rustre</w>
  <w f="60" flags="">râpé</w>
  <w f="60" flags="">réaffirmée</w>
+ <w f="60" flags="">réapparaitre</w>
  <w f="60" flags="">rébus</w>
  <w f="60" flags="">réceptionner</w>
  <w f="60" flags="">réceptionniste</w>
@@ -56571,7 +56540,6 @@
  <w f="59" flags="">guinées</w>
  <w f="59" flags="">gustative</w>
  <w f="59" flags="">guyanaise</w>
- <w f="59" flags="">guérillero</w>
  <w f="59" flags="">gélatineuse</w>
  <w f="59" flags="">génocidaire</w>
  <w f="59" flags="">généra</w>
@@ -56739,7 +56707,6 @@
  <w f="59" flags="abbreviation">l'Ems</w>
  <w f="59" flags="">l'Erie</w>
  <w f="59" flags="">l'Espagnole</w>
- <w f="59" flags="">l'Esterel</w>
  <w f="59" flags="">l'Estrémadure</w>
  <w f="59" flags="abbreviation">l'HMS</w>
  <w f="59" flags="abbreviation">l'ICC</w>
@@ -56853,7 +56820,6 @@
  <w f="59" flags="">laminoirs</w>
  <w f="59" flags="">lasso</w>
  <w f="59" flags="">latentes</w>
- <w f="59" flags="">laure</w>
  <w f="59" flags="">leishmaniose</w>
  <w f="59" flags="">lemmings</w>
  <w f="59" flags="">lenticulaire</w>
@@ -57083,7 +57049,6 @@
  <w f="59" flags="">pathologiste</w>
  <w f="59" flags="">pattern</w>
  <w f="59" flags="">pectoraux</w>
- <w f="59" flags="">pedigree</w>
  <w f="59" flags="">pendage</w>
  <w f="59" flags="">pensez</w>
  <w f="59" flags="">perfectif</w>
@@ -57296,7 +57261,6 @@
  <w f="59" flags="">rotin</w>
  <w f="59" flags="">rouage</w>
  <w f="59" flags="">rougir</w>
- <w f="59" flags="">râga</w>
  <w f="59" flags="">râteau</w>
  <w f="59" flags="">règnera</w>
  <w f="59" flags="">récollets</w>
@@ -57476,7 +57440,6 @@
  <w f="59" flags="">survive</w>
  <w f="59" flags="">surélever</w>
  <w f="59" flags="">suspicieux</w>
- <w f="59" flags="">sutras</w>
  <w f="59" flags="">suève</w>
  <w f="59" flags="">swaps</w>
  <w f="59" flags="">sycomore</w>
@@ -58677,7 +58640,6 @@
  <w f="58" flags="">d'antiquité</w>
  <w f="58" flags="">d'appendices</w>
  <w f="58" flags="">d'arrivées</w>
- <w f="58" flags="">d'artefacts</w>
  <w f="58" flags="">d'artisan</w>
  <w f="58" flags="">d'arête</w>
  <w f="58" flags="">d'assauts</w>
@@ -59844,7 +59806,6 @@
  <w f="58" flags="">savate</w>
  <w f="58" flags="">scalde</w>
  <w f="58" flags="">scandaleuses</w>
- <w f="58" flags="">scenarii</w>
  <w f="58" flags="">schwa</w>
  <w f="58" flags="">scolastiques</w>
  <w f="58" flags="">scripte</w>
@@ -61680,7 +61641,6 @@
  <w f="57" flags="">l'avertir</w>
  <w f="57" flags="">l'aéroclub</w>
  <w f="57" flags="">l'effectivité</w>
- <w f="57" flags="">l'electro</w>
  <w f="57" flags="">l'ellipsoïde</w>
  <w f="57" flags="">l'en-but</w>
  <w f="57" flags="">l'encéphale</w>
@@ -61831,7 +61791,6 @@
  <w f="57" flags="">multifonctions</w>
  <w f="57" flags="">multilatérale</w>
  <w f="57" flags="">multilatérales</w>
- <w f="57" flags="">multimedia</w>
  <w f="57" flags="">multiplexes</w>
  <w f="57" flags="">multipolaire</w>
  <w f="57" flags="">muons</w>
@@ -62271,7 +62230,6 @@
  <w f="57" flags="">savoisienne</w>
  <w f="57" flags="">saxo</w>
  <w f="57" flags="">scellement</w>
- <w f="57" flags="">scenario</w>
  <w f="57" flags="">schizophrènes</w>
  <w f="57" flags="">scintillement</w>
  <w f="57" flags="">scié</w>
@@ -63875,7 +63833,6 @@
  <w f="56" flags="">efficience</w>
  <w f="56" flags="">efficient</w>
  <w f="56" flags="">el-Cheikh</w>
- <w f="56" flags="">el-Kebir</w>
  <w f="56" flags="">embaucha</w>
  <w f="56" flags="">embusqué</w>
  <w f="56" flags="">emmenèrent</w>
@@ -64412,7 +64369,6 @@
  <w f="56" flags="">mentorat</w>
  <w f="56" flags="">menues</w>
  <w f="56" flags="">mercantiliste</w>
- <w f="56" flags="">meta</w>
  <w f="56" flags="">mi-pente</w>
  <w f="56" flags="">microbiologiste</w>
  <w f="56" flags="">micrométéorites</w>
@@ -65477,7 +65433,6 @@
  <w f="55" flags="abbreviation">HBC</w>
  <w f="55" flags="abbreviation">HII</w>
  <w f="55" flags="abbreviation">HP-UX</w>
- <w f="55" flags="abbreviation">HTA</w>
  <w f="55" flags="">Haden</w>
  <w f="55" flags="">Hainan</w>
  <w f="55" flags="">Hallstatt</w>
@@ -66416,7 +66371,6 @@
  <w f="55" flags="">d'Alta</w>
  <w f="55" flags="">d'Aman</w>
  <w f="55" flags="">d'Anagni</w>
- <w f="55" flags="">d'Angoulème</w>
  <w f="55" flags="">d'Anhalt-Dessau</w>
  <w f="55" flags="">d'Anticosti</w>
  <w f="55" flags="">d'Antigua</w>
@@ -66587,7 +66541,6 @@
  <w f="55" flags="">denticules</w>
  <w f="55" flags="">dessécher</w>
  <w f="55" flags="">destitua</w>
- <w f="55" flags="">devanagari</w>
  <w f="55" flags="">diabolo</w>
  <w f="55" flags="">dialoguent</w>
  <w f="55" flags="">diastolique</w>
@@ -66599,7 +66552,6 @@
  <w f="55" flags="">digipack</w>
  <w f="55" flags="">dilapide</w>
  <w f="55" flags="">diluant</w>
- <w f="55" flags="">dime</w>
  <w f="55" flags="">diptère</w>
  <w f="55" flags="">directivité</w>
  <w f="55" flags="">disculpé</w>
@@ -66838,7 +66790,6 @@
  <w f="55" flags="">flavescence</w>
  <w f="55" flags="">fleurettiste</w>
  <w f="55" flags="">flexionnelle</w>
- <w f="55" flags="">florès</w>
  <w f="55" flags="">fluo</w>
  <w f="55" flags="">fléchi</w>
  <w f="55" flags="">fondirent</w>
@@ -67156,7 +67107,6 @@
  <w f="55" flags="">l'ainée</w>
  <w f="55" flags="">l'ajournement</w>
  <w f="55" flags="">l'allocution</w>
- <w f="55" flags="">l'ambigüité</w>
  <w f="55" flags="">l'ambitieuse</w>
  <w f="55" flags="">l'angine</w>
  <w f="55" flags="">l'antifascisme</w>
@@ -67694,6 +67644,7 @@
  <w f="55" flags="">qu'Eugène</w>
  <w f="55" flags="">qu'Henry</w>
  <w f="55" flags="">qu'Ibn</w>
+ <w f="55" flags="">qu'apparait</w>
  <w f="55" flags="">qu'association</w>
  <w f="55" flags="">qu'enfant</w>
  <w f="55" flags="">qu'entraîneur-joueur</w>
@@ -69943,7 +69894,6 @@
  <w f="54" flags="">mennonite</w>
  <w f="54" flags="">mentent</w>
  <w f="54" flags="">mercy</w>
- <w f="54" flags="">mesa</w>
  <w f="54" flags="">meseta</w>
  <w f="54" flags="">mesurera</w>
  <w f="54" flags="">meublées</w>
@@ -70487,7 +70437,6 @@
  <w f="54" flags="">survola</w>
  <w f="54" flags="">suscitaient</w>
  <w f="54" flags="">susvisé</w>
- <w f="54" flags="">sutra</w>
  <w f="54" flags="">synchronique</w>
  <w f="54" flags="">synchronisme</w>
  <w f="54" flags="">syncopé</w>
@@ -72014,7 +71963,6 @@
  <w f="53" flags="">d'Ayen</w>
  <w f="53" flags="">d'Eberhard</w>
  <w f="53" flags="">d'Echéry</w>
- <w f="53" flags="">d'Ecouen</w>
  <w f="53" flags="">d'Efteling</w>
  <w f="53" flags="">d'Eichmann</w>
  <w f="53" flags="">d'Elisa</w>
@@ -73120,7 +73068,6 @@
  <w f="53" flags="">parpaings</w>
  <w f="53" flags="">partiale</w>
  <w f="53" flags="">partirait</w>
- <w f="53" flags="">paseo</w>
  <w f="53" flags="">passacaille</w>
  <w f="53" flags="">pataphysique</w>
  <w f="53" flags="">patibulaire</w>
@@ -74939,6 +74886,7 @@
  <w f="52" flags="">congeler</w>
  <w f="52" flags="">congélateurs</w>
  <w f="52" flags="">conseillera</w>
+ <w f="52">conseillerais</w>
  <w f="52" flags="">conseillères</w>
  <w f="52" flags="">consolidations</w>
  <w f="52" flags="">consoude</w>
@@ -75209,7 +75157,6 @@
  <w f="52" flags="">divergeant</w>
  <w f="52" flags="">divisera</w>
  <w f="52" flags="">doline</w>
- <w f="52" flags="">dona</w>
  <w f="52" flags="">donnât</w>
  <w f="52" flags="">dope</w>
  <w f="52" flags="">dormeurs</w>
@@ -75428,7 +75375,6 @@
  <w f="52" flags="">félibre</w>
  <w f="52" flags="">férue</w>
  <w f="52" flags="">fût-elle</w>
- <w f="52" flags="">fœhn</w>
  <w f="52" flags="">gaité</w>
  <w f="52" flags="">gallicans</w>
  <w f="52" flags="">galope</w>
@@ -77912,10 +77858,8 @@
  <w f="51" flags="">gaspillé</w>
  <w f="51" flags="">gauchisme</w>
  <w f="51" flags="">gaves</w>
- <w f="51" flags="">genera</w>
  <w f="51" flags="">genette</w>
  <w f="51" flags="">gente</w>
- <w f="51" flags="">gentile</w>
  <w f="51" flags="">gentium</w>
  <w f="51" flags="">gibbons</w>
  <w f="51" flags="">gicleur</w>
@@ -80527,7 +80471,6 @@
  <w f="50" flags="">liner</w>
  <w f="50" flags="">lingue</w>
  <w f="50" flags="">linnéen</w>
- <w f="50" flags="">linoleum</w>
  <w f="50" flags="">linoléum</w>
  <w f="50" flags="">linéarisation</w>
  <w f="50" flags="">lipase</w>
@@ -80564,7 +80507,6 @@
  <w f="50" flags="">magnétisation</w>
  <w f="50" flags="">magnéto</w>
  <w f="50" flags="">magnétohydrodynamique</w>
- <w f="50" flags="">mahârâja</w>
  <w f="50" flags="">majorer</w>
  <w f="50" flags="">majorettes</w>
  <w f="50" flags="">makhzen</w>
@@ -83070,7 +83012,6 @@
  <w f="49" flags="">l'Ambigu</w>
  <w f="49" flags="">l'Astra</w>
  <w f="49" flags="">l'Ecclésia</w>
- <w f="49" flags="">l'Estérel</w>
  <w f="49" flags="">l'IDH</w>
  <w f="49" flags="abbreviation">l'ISA</w>
  <w f="49" flags="abbreviation">l'InVS</w>
@@ -83417,7 +83358,6 @@
  <w f="49" flags="">octosyllabiques</w>
  <w f="49" flags="">offensés</w>
  <w f="49" flags="">oisiveté</w>
- <w f="49" flags="">omega</w>
  <w f="49" flags="">omoplate</w>
  <w f="49" flags="">oncogène</w>
  <w f="49" flags="">oncologie</w>
@@ -85347,7 +85287,6 @@
  <w f="48" flags="">césarisme</w>
  <w f="48" flags="">césures</w>
  <w f="48" flags="">côtoiera</w>
- <w f="48" flags="">d'Ares</w>
  <w f="48" flags="">d'Axa</w>
  <w f="48" flags="">d'Hemingway</w>
  <w f="48" flags="abbreviation">d'URL</w>
@@ -85768,7 +85707,6 @@
  <w f="48" flags="">gaucherie</w>
  <w f="48" flags="">gelant</w>
  <w f="48" flags="">gendarmeries</w>
- <w f="48" flags="">gene</w>
  <w f="48" flags="">genin</w>
  <w f="48" flags="">genouillères</w>
  <w f="48" flags="">gentianes</w>
@@ -85806,7 +85744,6 @@
  <w f="48" flags="">gueuse</w>
  <w f="48" flags="">guinée</w>
  <w f="48" flags="">gujarati</w>
- <w f="48" flags="">gurû</w>
  <w f="48" flags="">guéries</w>
  <w f="48" flags="">guérira</w>
  <w f="48" flags="">guérisse</w>
@@ -86135,7 +86072,6 @@
  <w f="48" flags="">lamentant</w>
  <w f="48" flags="">lamenter</w>
  <w f="48" flags="">laminée</w>
- <w f="48" flags="">lander</w>
  <w f="48" flags="">lands</w>
  <w f="48" flags="">langoureux</w>
  <w f="48" flags="">laotiennes</w>
@@ -86293,7 +86229,6 @@
  <w f="48" flags="">mésosphère</w>
  <w f="48" flags="">métalangage</w>
  <w f="48" flags="">mît</w>
- <w f="48" flags="">môns</w>
  <w f="48" flags="">n'aboutiront</w>
  <w f="48" flags="">n'accorda</w>
  <w f="48" flags="">n'apprit</w>
@@ -87166,7 +87101,6 @@
  <w f="47" flags="">Avellino</w>
  <w f="47" flags="">Avesnes</w>
  <w f="47" flags="">Ayutthaya</w>
- <w f="47" flags="abbreviation">BOF</w>
  <w f="47" flags="">Babin</w>
  <w f="47" flags="">Bagotville</w>
  <w f="47" flags="">Bainville</w>
@@ -87876,6 +87810,7 @@
  <w f="47" flags="">blitzkrieg</w>
  <w f="47" flags="">boards</w>
  <w f="47" flags="">boas</w>
+ <w f="47" flags="abbreviation">bof</w>
  <w f="47" flags="">bomba</w>
  <w f="47" flags="">bomber</w>
  <w f="47" flags="">bombés</w>
@@ -87988,7 +87923,6 @@
  <w f="47" flags="">clinicienne</w>
  <w f="47" flags="">cloisonnements</w>
  <w f="47" flags="">closerie</w>
- <w f="47" flags="">clot</w>
  <w f="47" flags="">cloîtrées</w>
  <w f="47" flags="">clématite</w>
  <w f="47" flags="">clôturera</w>
@@ -88967,7 +88901,6 @@
  <w f="47" flags="">pesanteurs</w>
  <w f="47" flags="">petit-duc</w>
  <w f="47" flags="">peupla</w>
- <w f="47" flags="">peña</w>
  <w f="47" flags="">phalangiste</w>
  <w f="47" flags="">pharmacocinétique</w>
  <w f="47" flags="">phasmes</w>
@@ -91145,7 +91078,6 @@
  <w f="46" flags="">impudique</w>
  <w f="46" flags="">impulsa</w>
  <w f="46" flags="">impulsifs</w>
- <w f="46" flags="">imâm</w>
  <w f="46" flags="abbreviation">ina</w>
  <w f="46" flags="">inapplicables</w>
  <w f="46" flags="">incandescents</w>
@@ -91300,7 +91232,6 @@
  <w f="46" flags="">l'ex-directeur</w>
  <w f="46" flags="">l'ex-épouse</w>
  <w f="46" flags="">l'examine</w>
- <w f="46" flags="">l'exigüité</w>
  <w f="46" flags="">l'expatriation</w>
  <w f="46" flags="">l'expliquent</w>
  <w f="46" flags="">l'exploratrice</w>
@@ -91441,13 +91372,11 @@
  <w f="46" flags="">martyriser</w>
  <w f="46" flags="">masseuse</w>
  <w f="46" flags="">mastectomie</w>
- <w f="46" flags="">mega</w>
  <w f="46" flags="">membrée</w>
  <w f="46" flags="">mentant</w>
  <w f="46" flags="">mentionnèrent</w>
  <w f="46" flags="">menuets</w>
  <w f="46" flags="">mer-sol</w>
- <w f="46" flags="">metro</w>
  <w f="46" flags="">mi-championnat</w>
  <w f="46" flags="">micocoulier</w>
  <w f="46" flags="">micro-algues</w>
@@ -93469,7 +93398,6 @@
  <w f="45" flags="">concouraient</w>
  <w f="45" flags="">concourra</w>
  <w f="45" flags="">condensations</w>
- <w f="45" flags="">condottieres</w>
  <w f="45" flags="">confectionna</w>
  <w f="45" flags="">configurées</w>
  <w f="45" flags="">conflagration</w>
@@ -96795,7 +96723,6 @@
  <w f="44" flags="">eschatologie</w>
  <w f="44" flags="">escomptées</w>
  <w f="44" flags="">espar</w>
- <w f="44" flags="">esperanto</w>
  <w f="44" flags="">espionnait</w>
  <w f="44" flags="">esquivant</w>
  <w f="44" flags="">essaimage</w>
@@ -97285,9 +97212,7 @@
  <w f="44" flags="">laryngite</w>
  <w f="44" flags="">laryngé</w>
  <w f="44" flags="">latéralité</w>
- <w f="44" flags="">lauré</w>
  <w f="44" flags="">laze</w>
- <w f="44" flags="">leges</w>
  <w f="44" flags="">lestées</w>
  <w f="44" flags="">lettrages</w>
  <w f="44" flags="">levantine</w>
@@ -97372,7 +97297,6 @@
  <w f="44" flags="">micronoyaux</w>
  <w f="44" flags="">miette</w>
  <w f="44" flags="">militarisés</w>
- <w f="44" flags="">millenium</w>
  <w f="44" flags="">millerandage</w>
  <w f="44" flags="">minant</w>
  <w f="44" flags="">mini-jupe</w>
@@ -99977,7 +99901,6 @@
  <w f="43" flags="">mnémoniques</w>
  <w f="43" flags="">mobilisera</w>
  <w f="43" flags="">modifieront</w>
- <w f="43" flags="">moitie</w>
  <w f="43" flags="">molester</w>
  <w f="43" flags="">mona</w>
  <w f="43" flags="">monistes</w>
@@ -101868,7 +101791,6 @@
  <w f="42" flags="">dichotomies</w>
  <w f="42" flags="">dichotomiques</w>
  <w f="42" flags="">dilapidations</w>
- <w f="42" flags="">dimes</w>
  <w f="42" flags="">diplodocus</w>
  <w f="42" flags="">disait-elle</w>
  <w f="42" flags="">discréditées</w>
@@ -102899,7 +102821,6 @@
  <w f="42" flags="">restituait</w>
  <w f="42" flags="">retracés</w>
  <w f="42" flags="">retranscrivent</w>
- <w f="42" flags="">retro</w>
  <w f="42" flags="">retrouvez</w>
  <w f="42" flags="">reversant</w>
  <w f="42" flags="">revirent</w>
@@ -104901,7 +104822,6 @@
  <w f="41" flags="">garde-temps</w>
  <w f="41" flags="">gardes-pêche</w>
  <w f="41" flags="">gastroentérite</w>
- <w f="41" flags="">gates</w>
  <w f="41" flags="">gaufrier</w>
  <w f="41" flags="">gaules</w>
  <w f="41" flags="">gaussiens</w>
@@ -105532,7 +105452,6 @@
  <w f="41" flags="">plafonnées</w>
  <w f="41" flags="">plaisanté</w>
  <w f="41" flags="">planctons</w>
- <w f="41" flags="">plantagenêt</w>
  <w f="41" flags="">plantera</w>
  <w f="41" flags="">plaqueminier</w>
  <w f="41" flags="">playa</w>
@@ -108603,7 +108522,6 @@
  <w f="40" flags="">radiatives</w>
  <w f="40" flags="">radiophare</w>
  <w f="40" flags="">radiosondes</w>
- <w f="40" flags="">raga</w>
  <w f="40" flags="">rages</w>
  <w f="40" flags="">rainbow</w>
  <w f="40" flags="">rainuré</w>
@@ -108827,7 +108745,6 @@
  <w f="40" flags="">sarclage</w>
  <w f="40" flags="">satori</w>
  <w f="40" flags="">saturations</w>
- <w f="40" flags="">satî</w>
  <w f="40" flags="">saupoudrage</w>
  <w f="40" flags="">saurez</w>
  <w f="40" flags="">sauvez</w>
@@ -111688,7 +111605,6 @@
  <w f="39" flags="">monèmes</w>
  <w f="39" flags="">moralisant</w>
  <w f="39" flags="">moro</w>
- <w f="39" flags="">moré</w>
  <w f="39" flags="">mosaïsme</w>
  <w f="39" flags="">moteur-boîte</w>
  <w f="39" flags="">motivèrent</w>
@@ -111728,6 +111644,7 @@
  <w f="39" flags="">n'adopta</w>
  <w f="39" flags="">n'agissant</w>
  <w f="39" flags="">n'allèrent</w>
+ <w f="39">n'apportez</w>
  <w f="39" flags="">n'approcha</w>
  <w f="39" flags="">n'atteindrait</w>
  <w f="39" flags="">n'empêchaient</w>
@@ -115033,7 +114950,6 @@
  <w f="37" flags="">croyait-on</w>
  <w f="37" flags="">cryptogrammes</w>
  <w f="37" flags="">créancière</w>
- <w f="37" flags="">crémerie</w>
  <w f="37" flags="">crénelages</w>
  <w f="37" flags="">crépue</w>
  <w f="37" flags="">crétacées</w>
@@ -115728,7 +115644,6 @@
  <w f="37" flags="">harkas</w>
  <w f="37" flags="">harnachements</w>
  <w f="37" flags="">hast</w>
- <w f="37" flags="">haste</w>
  <w f="37" flags="">hellénisants</w>
  <w f="37" flags="">hennin</w>
  <w f="37" flags="">herbagers</w>
@@ -118031,7 +117946,6 @@
  <w f="36" flags="">doutons</w>
  <w f="36" flags="">doyennes</w>
  <w f="36" flags="">drives</w>
- <w f="36" flags="">drivé</w>
  <w f="36" flags="">droguées</w>
  <w f="36" flags="">droitières</w>
  <w f="36" flags="">ductus</w>
@@ -119687,6 +119601,7 @@
  <w f="35" flags="">appelleraient</w>
  <w f="35" flags="">appoggiatures</w>
  <w f="35" flags="">appointée</w>
+ <w f="35">apporteriez</w>
  <w f="35" flags="">apposèrent</w>
  <w f="35" flags="">apprendraient</w>
  <w f="35" flags="">approprie</w>
@@ -120848,7 +120763,6 @@
  <w f="35" flags="">fantasmatiques</w>
  <w f="35" flags="">fardage</w>
  <w f="35" flags="">faro</w>
- <w f="35" flags="">fasciculés</w>
  <w f="35" flags="">fascinaient</w>
  <w f="35" flags="">fascisation</w>
  <w f="35" flags="">fayard</w>
@@ -121985,7 +121899,6 @@
  <w f="35" flags="">pastore</w>
  <w f="35" flags="">pataude</w>
  <w f="35" flags="">patauge</w>
- <w f="35" flags="">pati</w>
  <w f="35" flags="">paturon</w>
  <w f="35" flags="">paturons</w>
  <w f="35" flags="">patènes</w>
@@ -123976,7 +123889,6 @@
  <w f="34" flags="">déviateur</w>
  <w f="34" flags="">dîna</w>
  <w f="34" flags="">ectopiques</w>
- <w f="34" flags="">eden</w>
  <w f="34" flags="">effectueraient</w>
  <w f="34" flags="">effraies</w>
  <w f="34" flags="">effritée</w>
@@ -124531,7 +124443,6 @@
  <w f="34" flags="">mestres</w>
  <w f="34" flags="">mesureront</w>
  <w f="34" flags="">meublent</w>
- <w f="34" flags="">mi-aout</w>
  <w f="34" flags="">mi-humaines</w>
  <w f="34" flags="">mi-voix</w>
  <w f="34" flags="">micelle</w>
@@ -127052,7 +126963,6 @@
  <w f="33" flags="">pavanes</w>
  <w f="33" flags="">pavant</w>
  <w f="33" flags="">pavimenteux</w>
- <w f="33" flags="">penelope</w>
  <w f="33" flags="">pensionna</w>
  <w f="33" flags="">pentacles</w>
  <w f="33" flags="">perborate</w>
@@ -131024,7 +130934,6 @@
  <w f="31" flags="">châtelaines</w>
  <w f="31" flags="">chènevières</w>
  <w f="31" flags="">cicatrisante</w>
- <w f="31" flags="">cicéro</w>
  <w f="31" flags="">cicéronienne</w>
  <w f="31" flags="">cimentant</w>
  <w f="31" flags="">cincles</w>
@@ -131800,7 +131709,6 @@
  <w f="31" flags="">grattés</w>
  <w f="31" flags="">gravissaient</w>
  <w f="31" flags="">gravois</w>
- <w f="31" flags="">greco</w>
  <w f="31" flags="">grecs-orthodoxes</w>
  <w f="31" flags="">greffera</w>
  <w f="31" flags="">grenadines</w>
@@ -131965,7 +131873,6 @@
  <w f="31" flags="">ivresses</w>
  <w f="31" flags="">j'écoutais</w>
  <w f="31" flags="">jaillie</w>
- <w f="31" flags="">jaina</w>
  <w f="31" flags="">jamaïquaines</w>
  <w f="31" flags="">janus</w>
  <w f="31" flags="">javelles</w>
@@ -132169,7 +132076,6 @@
  <w f="31" flags="">lattée</w>
  <w f="31" flags="">laudatives</w>
  <w f="31" flags="">lavandins</w>
- <w f="31" flags="">leger</w>
  <w f="31" flags="">levantines</w>
  <w f="31" flags="">lexicologiques</w>
  <w f="31" flags="">libéralisa</w>
@@ -132223,7 +132129,6 @@
  <w f="31" flags="">macro-algues</w>
  <w f="31" flags="">macrocéphale</w>
  <w f="31" flags="">macèrent</w>
- <w f="31" flags="">maelstrom</w>
  <w f="31" flags="">magnésiennes</w>
  <w f="31" flags="">magnétisées</w>
  <w f="31" flags="">mahométan</w>
@@ -134732,7 +134637,6 @@
  <w f="30" flags="">farcit</w>
  <w f="30" flags="">fardé</w>
  <w f="30" flags="">fasciation</w>
- <w f="30" flags="">fasciculé</w>
  <w f="30" flags="">fascinations</w>
  <w f="30" flags="">faucha</w>
  <w f="30" flags="">fausse-renoncule</w>
@@ -136328,6 +136232,8 @@
  <w f="30" flags="">sérails</w>
  <w f="30" flags="">sérialisée</w>
  <w f="30" flags="">séricigènes</w>
+ <w f="30">t'adresser</w>
+ <w f="30">t'adresses</w>
  <w f="30" flags="">t'aider</w>
  <w f="30" flags="">t'emmerde</w>
  <w f="30" flags="">taboues</w>
@@ -137547,7 +137453,6 @@
  <w f="28" flags="">d'arrangeurs</w>
  <w f="28" flags="">d'arrhes</w>
  <w f="28" flags="">d'arrière-plans</w>
- <w f="28" flags="">d'artefact</w>
  <w f="28" flags="">d'arythmies</w>
  <w f="28" flags="">d'assurance-crédit</w>
  <w f="28" flags="">d'astigmatisme</w>
@@ -138268,7 +138173,6 @@
  <w f="28" flags="">herbager</w>
  <w f="28" flags="">herborisa</w>
  <w f="28" flags="">herculéen</w>
- <w f="28" flags="">herpes</w>
  <w f="28" flags="">hiatale</w>
  <w f="28" flags="">histidines</w>
  <w f="28" flags="">hivernages</w>
@@ -146791,11 +146695,9 @@
  <w f="25" flags="">fédé</w>
  <w f="25" flags="">fédéral-provincial</w>
  <w f="25" flags="">félicitaient</w>
- <w f="25" flags="">féra</w>
  <w f="25" flags="">gagnais</w>
  <w f="25" flags="">gaillets</w>
  <w f="25" flags="">gainiers</w>
- <w f="25" flags="">gallé</w>
  <w f="25" flags="">galonnés</w>
  <w f="25" flags="">galoubet-tambourin</w>
  <w f="25" flags="">galoubets</w>
@@ -147598,7 +147500,6 @@
  <w f="25" flags="">maçonnes</w>
  <w f="25" flags="">maïzena</w>
  <w f="25" flags="">melba</w>
- <w f="25" flags="">melissa</w>
  <w f="25" flags="">menteuses</w>
  <w f="25" flags="">mentholée</w>
  <w f="25" flags="">mentionnerons</w>
@@ -147712,7 +147613,6 @@
  <w f="25" flags="">mouvaient</w>
  <w f="25" flags="">moyen-bavarois</w>
  <w f="25" flags="">moyen-néerlandais</w>
- <w f="25" flags="">moëres</w>
  <w f="25" flags="">mugir</w>
  <w f="25" flags="">multivariées</w>
  <w f="25" flags="">musé</w>
@@ -151397,7 +151297,6 @@
  <w f="23" flags="">dandiner</w>
  <w f="23" flags="">dansais</w>
  <w f="23" flags="">danse-contact</w>
- <w f="23" flags="">daphne</w>
  <w f="23" flags="">dardes</w>
  <w f="23" flags="">daring</w>
  <w f="23" flags="">dariques</w>
@@ -152474,7 +152373,6 @@
  <w f="23" flags="">kantiens</w>
  <w f="23" flags="">keno</w>
  <w f="23" flags="">khamsin</w>
- <w f="23" flags="">khân</w>
  <w f="23" flags="">kiki</w>
  <w f="23" flags="">kilkenny</w>
  <w f="23" flags="">kinder</w>
@@ -152886,7 +152784,6 @@
  <w f="23" flags="">lacèrent</w>
  <w f="23" flags="">lacée</w>
  <w f="23" flags="">lae</w>
- <w f="23" flags="">laiche</w>
  <w f="23" flags="">laideron</w>
  <w f="23" flags="">laisse-toi</w>
  <w f="23" flags="">laisseras</w>
@@ -153811,7 +153708,6 @@
  <w f="23" flags="">poissonnières</w>
  <w f="23" flags="">poissé</w>
  <w f="23" flags="">polacre</w>
- <w f="23" flags="">polaroid</w>
  <w f="23" flags="">polatouches</w>
  <w f="23" flags="">polissons</w>
  <w f="23" flags="">pollupostage</w>
@@ -154711,7 +154607,6 @@
  <w f="23" flags="">servocommande</w>
  <w f="23" flags="">servomécanismes</w>
  <w f="23" flags="">sexagésimaux</w>
- <w f="23" flags="">shâh</w>
  <w f="23" flags="">siccatifs</w>
  <w f="23" flags="">sifflaient</w>
  <w f="23" flags="">sillages</w>
@@ -155963,6 +155858,7 @@
  <w f="21" flags="">Motte-Saint-Valentin</w>
  <w f="21" flags="">Moulin-Brûlé</w>
  <w f="21" flags="">Musser</w>
+ <w f="21" flags="">Mélissa</w>
  <w f="21" flags="">Métro-Richelieu</w>
  <w f="21" flags="">Mézières-en-Gâtinais</w>
  <w f="21" flags="">Napoléon-Charles</w>
@@ -158839,7 +158735,6 @@
  <w f="21" flags="">hee</w>
  <w f="21" flags="">heller</w>
  <w f="21" flags="">hellénismes</w>
- <w f="21" flags="">herbés</w>
  <w f="21" flags="">herve</w>
  <w f="21" flags="">heurtons</w>
  <w f="21" flags="">hiberné</w>
@@ -159635,7 +159530,6 @@
  <w f="21" flags="">lactifères</w>
  <w f="21" flags="">lacéra</w>
  <w f="21" flags="">laguerre</w>
- <w f="21" flags="">laiches</w>
  <w f="21" flags="">laisserais</w>
  <w f="21" flags="">lamentos</w>
  <w f="21" flags="">lamentèrent</w>
@@ -160062,7 +159956,6 @@
  <w f="21" flags="">méga-corporation</w>
  <w f="21" flags="">mélamine-formaldéhyde</w>
  <w f="21" flags="">mélangeront</w>
- <w f="21" flags="">mélissa</w>
  <w f="21" flags="">mémorisait</w>
  <w f="21" flags="">ménologes</w>
  <w f="21" flags="">ménopausique</w>
@@ -160666,7 +160559,6 @@
  <w f="21" flags="">peinard</w>
  <w f="21" flags="">peinez</w>
  <w f="21" flags="">peinturer</w>
- <w f="21" flags="">pelagos</w>
  <w f="21" flags="">pelliculée</w>
  <w f="21" flags="">peltaste</w>
  <w f="21" flags="">pelté</w>
@@ -165431,7 +165323,6 @@
  <w f="18" flags="">crédit-carbone</w>
  <w f="18" flags="">créditera</w>
  <w f="18" flags="">crédulités</w>
- <w f="18" flags="">crémeries</w>
  <w f="18" flags="">crénelées-dentelées</w>
  <w f="18" flags="">créolisés</w>
  <w f="18" flags="">créosotes</w>
@@ -170056,7 +169947,6 @@
  <w f="18" flags="">position-impulsion</w>
  <w f="18" flags="">postulations</w>
  <w f="18" flags="">posât</w>
- <w f="18" flags="">potencé</w>
  <w f="18" flags="">potentialisé</w>
  <w f="18" flags="">potenza</w>
  <w f="18" flags="">potestative</w>
@@ -172484,7 +172374,6 @@
  <w f="18" flags="">écartes</w>
  <w f="18" flags="">échafaudant</w>
  <w f="18" flags="">échafaudèrent</w>
- <w f="18" flags="">échancre</w>
  <w f="18" flags="">échangeons</w>
  <w f="18" flags="">échangeraient</w>
  <w f="18" flags="">écharper</w>
@@ -172751,7 +172640,6 @@
  <w f="15" flags="">Antoinette-Élisabeth</w>
  <w f="15" flags="">Anvers-Bruxelles</w>
  <w f="15" flags="">Anvers-Wavre</w>
- <w f="15" flags="">Apportez-moi</w>
  <w f="15" flags="">Arbre-Saint-Pierre</w>
  <w f="15" flags="">Ardenne-Rives-de-Meuse</w>
  <w f="15" flags="">Argas</w>
@@ -173065,6 +172953,7 @@
  <w f="15" flags="">Château-Voué</w>
  <w f="15" flags="">Château-sur-Aisne</w>
  <w f="15" flags="">Chérasse</w>
+ <w f="15" flags="">Cicero</w>
  <w f="15" flags="">Cinq-Chemins</w>
  <w f="15" flags="">Cinq-Moulins</w>
  <w f="15" flags="">Cinq-Provinces</w>
@@ -174677,6 +174566,7 @@
  <w f="15" flags="">apporte-t-il</w>
  <w f="15" flags="">apporterez</w>
  <w f="15" flags="">apportes</w>
+ <w f="15" flags="">apportez-moi</w>
  <w f="15" flags="">apprend-t-on</w>
  <w f="15" flags="">apprenti-dessinateur</w>
  <w f="15" flags="">apprenti-pâtissier</w>
@@ -175926,8 +175816,6 @@
  <w f="15" flags="">ciboriums</w>
  <w f="15" flags="">cicatrisations</w>
  <w f="15" flags="">cicatrisés</w>
- <w f="15" flags="">cicero</w>
- <w f="15" flags="">cicéros</w>
  <w f="15" flags="">ciel-terre</w>
  <w f="15" flags="">cillées</w>
  <w f="15" flags="">cimentera</w>
@@ -179207,7 +179095,6 @@
  <w f="15" flags="">gemmologiste</w>
  <w f="15" flags="">gentillets</w>
  <w f="15" flags="">gentillettes</w>
- <w f="15" flags="">genépi</w>
  <w f="15" flags="">gercées</w>
  <w f="15" flags="">germanisa</w>
  <w f="15" flags="">germinaux</w>
@@ -179448,7 +179335,6 @@
  <w f="15" flags="">heng</w>
  <w f="15" flags="">hennissant</w>
  <w f="15" flags="">heptagones</w>
- <w f="15" flags="">herbé</w>
  <w f="15" flags="">herméticité</w>
  <w f="15" flags="">herri</w>
  <w f="15" flags="">hersages</w>
@@ -180957,7 +180843,6 @@
  <w f="15" flags="">limitez</w>
  <w f="15" flags="">limonites</w>
  <w f="15" flags="">limée</w>
- <w f="15" flags="">limón</w>
  <w f="15" flags="">lingotières</w>
  <w f="15" flags="">liniers</w>
  <w f="15" flags="">linotypie</w>
@@ -182796,7 +182681,6 @@
  <w f="15" flags="">passage-clef</w>
  <w f="15" flags="">passagers-kilomètres</w>
  <w f="15" flags="">passassent</w>
- <w f="15" flags="">passat</w>
  <w f="15" flags="">passe-plats</w>
  <w f="15" flags="">passe-rose</w>
  <w f="15" flags="">passementière</w>
@@ -182844,7 +182728,6 @@
  <w f="15" flags="">pelas</w>
  <w f="15" flags="">pelliculées</w>
  <w f="15" flags="">pelotonnée</w>
- <w f="15" flags="">peléen</w>
  <w f="15" flags="">peléennes</w>
  <w f="15" flags="">penchât</w>
  <w f="15" flags="">pense-il</w>
@@ -182868,7 +182751,6 @@
  <w f="15" flags="">perdit-il</w>
  <w f="15" flags="">perdureraient</w>
  <w f="15" flags="">perdîmes</w>
- <w f="15" flags="">peres</w>
  <w f="15" flags="">perfectionneront</w>
  <w f="15" flags="">perfoliées</w>
  <w f="15" flags="">perforera</w>
@@ -183005,7 +182887,6 @@
  <w f="15" flags="">planchera</w>
  <w f="15" flags="">planchéiée</w>
  <w f="15" flags="">planeuse</w>
- <w f="15" flags="">plantagenet</w>
  <w f="15" flags="">planteraient</w>
  <w f="15" flags="">planteuses</w>
  <w f="15" flags="">plastiqueurs</w>
@@ -187747,7 +187628,6 @@
  <w f="10" flags="">côtoyez</w>
  <w f="10" flags="">d'Agnat</w>
  <w f="10" flags="">d'Albert-le-Grand</w>
- <w f="10" flags="">d'Ales</w>
  <w f="10" flags="">d'Algérien</w>
  <w f="10" flags="">d'Angoumois-infanterie</w>
  <w f="10" flags="">d'Anonyme</w>
@@ -188046,7 +187926,6 @@
  <w f="10" flags="">ferryboat</w>
  <w f="10" flags="">feule</w>
  <w f="10" flags="">fichier-clef</w>
- <w f="10" flags="">fieffe</w>
  <w f="10" flags="">fieffer</w>
  <w f="10" flags="">fies</w>
  <w f="10" flags="">fieu</w>
@@ -188231,7 +188110,6 @@
  <w f="10" flags="">hava</w>
  <w f="10" flags="">havir</w>
  <w f="10" flags="">hebdomadiers</w>
- <w f="10" flags="">hela</w>
  <w f="10" flags="">helminthe</w>
  <w f="10" flags="">henni</w>
  <w f="10" flags="">herzégovine</w>
@@ -188729,7 +188607,6 @@
  <w f="10" flags="">malpolis</w>
  <w f="10" flags="">malta</w>
  <w f="10" flags="">malter</w>
- <w f="10" flags="">malé</w>
  <w f="10" flags="">mancha</w>
  <w f="10" flags="">mangano</w>
  <w f="10" flags="">mante-religieuse</w>
@@ -189012,7 +188889,6 @@
  <w f="10" flags="">pastelle</w>
  <w f="10" flags="">pasteurienne</w>
  <w f="10" flags="">pastillas</w>
- <w f="10" flags="">paséo</w>
  <w f="10" flags="">patachons</w>
  <w f="10" flags="">patay</w>
  <w f="10" flags="">patoche</w>
@@ -191229,7 +191105,6 @@
  <w f="1" flags="">miton</w>
  <w f="1" flags="">moblots</w>
  <w f="1" flags="">moch</w>
- <w f="1" flags="">moeres</w>
  <w f="1" flags="">moha</w>
  <w f="1" flags="">moisant</w>
  <w f="1" flags="">moise</w>
@@ -191271,7 +191146,6 @@
  <w f="1" flags="">mémère</w>
  <w f="1" flags="">ménine</w>
  <w f="1" flags="">ményanthe</w>
- <w f="1" flags="">mésa</w>
  <w f="1" flags="">métra</w>
  <w f="1" flags="">métromanie</w>
  <w f="1" flags="">mézeray</w>
@@ -191837,6 +191711,7 @@
  <w f="0" flags="e">gouine</w>
  <w f="0" flags="n">gousse</w>
  <w f="0" flags="n">gémissement</w>
+ <w f="0">haha</w>
  <w f="0" flags="n">hardcore</w>
  <w f="0" flags="n">hermaphrodite</w>
  <w f="0" flags="e">homo</w>
diff --git a/java/Android.mk b/java/Android.mk
index 52cc18b..364973b 100644
--- a/java/Android.mk
+++ b/java/Android.mk
@@ -28,9 +28,7 @@
 # We want to install libjni_latinime.so to the system partition if LatinIME gets installed.
 LOCAL_REQUIRED_MODULES := libjni_latinime
 
-LOCAL_STATIC_JAVA_LIBRARIES := android-common
-LOCAL_STATIC_JAVA_LIBRARIES += inputmethod-common
-LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4
+LOCAL_STATIC_JAVA_LIBRARIES := android-common inputmethod-common android-support-v4
 
 # Do not compress dictionary files to mmap dict data runtime
 LOCAL_AAPT_FLAGS := -0 .dict
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index 06d852b..3e80de2 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -1,7 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         coreApp="true"
         package="com.android.inputmethod.latin">
 
+    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="16" />
+
     <uses-permission android:name="android.permission.VIBRATE"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
@@ -10,7 +28,8 @@
     <application android:label="@string/aosp_android_keyboard_ime_name"
             android:icon="@drawable/ic_ime_settings"
             android:backupAgent="BackupAgent"
-            android:killAfterRestore="false">
+            android:killAfterRestore="false"
+            android:supportsRtl="true">
 
         <service android:name="LatinIME"
                 android:label="@string/aosp_android_keyboard_ime_name"
diff --git a/java/proguard.flags b/java/proguard.flags
index 34e23aa..ac5b7df 100644
--- a/java/proguard.flags
+++ b/java/proguard.flags
@@ -3,10 +3,6 @@
   <init>(...);
 }
 
--keep class com.android.inputmethod.latin.Flag {
-  *;
-}
-
 -keep class com.android.inputmethod.keyboard.ProximityInfo {
   <init>(com.android.inputmethod.keyboard.ProximityInfo);
 }
@@ -24,11 +20,19 @@
   boolean equalsIgnoreCase(...);
 }
 
+-keep class com.android.inputmethod.latin.InputPointers {
+  *;
+}
+
+-keep class com.android.inputmethod.latin.ResizableIntArray {
+  *;
+}
+
 -keep class com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsFragment {
   *;
 }
 
--keep class com.android.inputmethod.keyboard.LatinKeyboardView {
+-keep class com.android.inputmethod.keyboard.MainKeyboardView {
   # Keep getter/setter methods for ObjectAnimator
   int getLanguageOnSpacebarAnimAlpha();
   void setLanguageOnSpacebarAnimAlpha(int);
@@ -40,14 +44,13 @@
   <init>(...);
 }
 
--keep class com.android.inputmethod.latin.ResearchLogger {
-  void setLogFileManager(...);
-  void clearAll();
-  com.android.inputmethod.latin.ResearchLogger$LogFileManager getLogFileManager();
+-keepclasseswithmembernames class * {
+    native <methods>;
 }
 
--keep class com.android.inputmethod.latin.ResearchLogger$LogFileManager {
-  java.lang.String getContents();
+-keep class com.android.inputmethod.research.ResearchLogger {
+  void flush();
+  void publishCurrentLogUnit(...);
 }
 
 -keep class com.android.inputmethod.keyboard.KeyboardLayoutSet$Builder {
diff --git a/java/res/drawable/btn_keyboard_key.xml b/java/res/drawable/btn_keyboard_key.xml
index 45578e5..797bc10 100644
--- a/java/res/drawable/btn_keyboard_key.xml
+++ b/java/res/drawable/btn_keyboard_key.xml
@@ -17,8 +17,8 @@
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
 
     <!-- Toggle keys. Use checkable/checked state. -->
-    
-    <item android:state_checkable="true" android:state_checked="true" 
+
+    <item android:state_checkable="true" android:state_checked="true"
           android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_pressed_on" />
     <item android:state_checkable="true" android:state_pressed="true"
@@ -34,5 +34,5 @@
           android:drawable="@drawable/btn_keyboard_key_pressed" />
     <item
           android:drawable="@drawable/btn_keyboard_key_normal" />
-          
+
 </selector>
diff --git a/java/res/drawable/keyboard_key_feedback.xml b/java/res/drawable/keyboard_key_feedback.xml
index 159ba86..397e948 100644
--- a/java/res/drawable/keyboard_key_feedback.xml
+++ b/java/res/drawable/keyboard_key_feedback.xml
@@ -14,9 +14,11 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_long_pressable="true"
-            android:drawable="@drawable/keyboard_key_feedback_more_background" />
-
+<selector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <item latin:state_has_morekeys="true"
+          android:drawable="@drawable/keyboard_key_feedback_more_background" />
     <item android:drawable="@drawable/keyboard_key_feedback_background" />
 </selector>
diff --git a/java/res/drawable/keyboard_key_feedback_ics.xml b/java/res/drawable/keyboard_key_feedback_ics.xml
index 04c8679..3c8850e 100644
--- a/java/res/drawable/keyboard_key_feedback_ics.xml
+++ b/java/res/drawable/keyboard_key_feedback_ics.xml
@@ -14,8 +14,23 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_long_pressable="true"
-            android:drawable="@drawable/keyboard_key_feedback_more_background_holo" />
+<selector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <!-- Left edge -->
+    <item latin:state_left_edge="true" latin:state_has_morekeys="true"
+          android:drawable="@drawable/keyboard_key_feedback_left_more_background_holo" />
+    <item latin:state_left_edge="true"
+          android:drawable="@drawable/keyboard_key_feedback_left_background_holo" />
+
+    <!-- Right edge -->
+    <item latin:state_right_edge="true" latin:state_has_morekeys="true"
+          android:drawable="@drawable/keyboard_key_feedback_right_more_background_holo" />
+    <item latin:state_right_edge="true"
+          android:drawable="@drawable/keyboard_key_feedback_right_background_holo" />
+
+    <item latin:state_has_morekeys="true"
+          android:drawable="@drawable/keyboard_key_feedback_more_background_holo" />
     <item android:drawable="@drawable/keyboard_key_feedback_background_holo" />
 </selector>
diff --git a/java/res/drawable/keyboard_key_feedback_left_ics.xml b/java/res/drawable/keyboard_key_feedback_left_ics.xml
deleted file mode 100644
index b68b37f..0000000
--- a/java/res/drawable/keyboard_key_feedback_left_ics.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_long_pressable="true"
-            android:drawable="@drawable/keyboard_key_feedback_left_more_background_holo" />
-    <item android:drawable="@drawable/keyboard_key_feedback_left_background_holo" />
-</selector>
diff --git a/java/res/drawable/keyboard_key_feedback_right_ics.xml b/java/res/drawable/keyboard_key_feedback_right_ics.xml
deleted file mode 100644
index 830678a..0000000
--- a/java/res/drawable/keyboard_key_feedback_right_ics.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_long_pressable="true"
-            android:drawable="@drawable/keyboard_key_feedback_right_more_background_holo" />
-    <item android:drawable="@drawable/keyboard_key_feedback_right_background_holo" />
-</selector>
diff --git a/java/res/layout/input_view.xml b/java/res/layout/input_view.xml
index 3863534..40eff38 100644
--- a/java/res/layout/input_view.xml
+++ b/java/res/layout/input_view.xml
@@ -43,20 +43,20 @@
             android:layout_width="@dimen/suggestions_strip_padding"
             android:layout_height="@dimen/suggestions_strip_height"
             style="?attr/suggestionsStripBackgroundStyle" />
-        <com.android.inputmethod.latin.suggestions.SuggestionsView
-            android:id="@+id/suggestions_view"
+        <com.android.inputmethod.latin.suggestions.SuggestionStripView
+            android:id="@+id/suggestion_strip_view"
             android:layout_weight="1.0"
             android:layout_width="0dp"
             android:layout_height="@dimen/suggestions_strip_height"
             android:gravity="center_vertical"
-            style="?attr/suggestionsViewStyle" />
+            style="?attr/suggestionStripViewStyle" />
         <View
             android:layout_width="@dimen/suggestions_strip_padding"
             android:layout_height="@dimen/suggestions_strip_height"
             style="?attr/suggestionsStripBackgroundStyle" />
     </LinearLayout>
 
-    <com.android.inputmethod.keyboard.LatinKeyboardView
+    <com.android.inputmethod.keyboard.MainKeyboardView
         android:id="@+id/keyboard_view"
         android:layout_alignParentBottom="true"
         android:layout_width="match_parent"
diff --git a/java/res/layout/key_preview.xml b/java/res/layout/key_preview.xml
index 6ed892e..2fcd0c4 100644
--- a/java/res/layout/key_preview.xml
+++ b/java/res/layout/key_preview.xml
@@ -20,8 +20,8 @@
 
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="80dp"
-    android:textSize="40dp"
+    android:layout_height="wrap_content"
+    android:background="@drawable/keyboard_key_feedback"
     android:minWidth="32dp"
     android:gravity="center"
-    />
+/>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/layout/key_preview_ics.xml
similarity index 70%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/layout/key_preview_ics.xml
index c0c45dd..222e884 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/layout/key_preview_ics.xml
@@ -18,9 +18,10 @@
 */
 -->
 
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@drawable/keyboard_key_feedback_ics"
+    android:minWidth="32dp"
+    android:gravity="center"
+/>
diff --git a/java/res/layout/research_feedback_activity.xml b/java/res/layout/research_feedback_activity.xml
new file mode 100644
index 0000000..a6b8b8a
--- /dev/null
+++ b/java/res/layout/research_feedback_activity.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<com.android.inputmethod.research.FeedbackLayout
+     xmlns:android="http://schemas.android.com/apk/res/android"
+     android:layout_width="match_parent"
+     android:layout_height="wrap_content"
+     android:orientation="vertical"
+     android:id="@+id/research_feedback_layout"
+>
+
+    <fragment
+          android:id="@+id/research_feedback_fragment"
+          android:name="com.android.inputmethod.research.FeedbackFragment"
+          android:layout_width="match_parent"
+          android:layout_height="wrap_content"
+    />
+</com.android.inputmethod.research.FeedbackLayout>
diff --git a/java/res/layout/research_feedback_fragment_layout.xml b/java/res/layout/research_feedback_fragment_layout.xml
new file mode 100644
index 0000000..cc04ced
--- /dev/null
+++ b/java/res/layout/research_feedback_fragment_layout.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+     xmlns:android="http://schemas.android.com/apk/res/android"
+     android:layout_width="fill_parent"
+     android:layout_height="fill_parent"
+     android:orientation="vertical"
+>
+
+    <!-- Mimic a dialog title.  Necessary since the dialog is actually an activity, so the normal
+        dialog title construction code is not available. -->
+    <LinearLayout
+         android:layout_width="match_parent"
+         android:layout_height="wrap_content"
+         android:orientation="vertical"
+    >
+        <com.android.internal.widget.DialogTitle
+            style="?android:attr/windowTitleStyle"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:layout_width="match_parent"
+            android:layout_height="64dip"
+            android:layout_marginLeft="16dip"
+            android:layout_marginRight="16dip"
+            android:gravity="center_vertical|left"
+            android:text="@string/research_feedback_dialog_title" />
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="2dip"
+            android:background="@android:color/holo_blue_light" />
+    </LinearLayout>
+
+    <EditText
+        android:id="@+id/research_feedback_contents"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_gravity="fill_horizontal|center_vertical"
+        android:layout_marginLeft="8dip"
+        android:layout_marginRight="8dip"
+        android:layout_marginBottom="8dip"
+        android:layout_marginTop="8dip"
+        android:lines="2"
+        android:hint="@string/research_feedback_hint"
+        android:inputType="textMultiLine"
+        android:imeOptions="flagNoFullscreen"
+    >
+        <requestFocus />
+    </EditText>
+
+    <CheckBox
+        android:id="@+id/research_feedback_include_history"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_marginBottom="8dip"
+        android:checked="true"
+        android:text="@string/research_feedback_include_history_label"
+    />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:divider="?android:attr/dividerHorizontal"
+        android:showDividers="beginning"
+        android:dividerPadding="0dip"
+    >
+        <LinearLayout
+            style="?android:attr/buttonBarStyle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:measureWithLargestChild="true"
+        >
+            <Button
+                android:id="@+id/research_feedback_cancel_button"
+                android:layout_width="0dip"
+                android:layout_gravity="left"
+                android:layout_weight="1"
+                android:maxLines="2"
+                style="?android:attr/buttonBarButtonStyle"
+                android:textSize="14sp"
+                android:text="@string/research_feedback_cancel"
+                android:layout_height="wrap_content"
+            />
+            <Button
+                android:id="@+id/research_feedback_send_button"
+                android:layout_width="0dip"
+                android:layout_gravity="right"
+                android:layout_weight="1"
+                android:maxLines="2"
+                style="?android:attr/buttonBarButtonStyle"
+                android:textSize="14sp"
+                android:text="@string/research_feedback_send"
+                android:layout_height="wrap_content"
+            />
+        </LinearLayout>
+    </LinearLayout>
+</LinearLayout>
diff --git a/java/res/layout/research_feedback_layout.xml b/java/res/layout/research_feedback_layout.xml
new file mode 100644
index 0000000..bacd191
--- /dev/null
+++ b/java/res/layout/research_feedback_layout.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+     xmlns:android="http://schemas.android.com/apk/res/android"
+     android:layout_width="fill_parent"
+     android:layout_height="fill_parent"
+     android:orientation="vertical"
+>
+
+    <EditText
+        android:id="@+id/research_feedback_contents"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_gravity="fill_horizontal|center_vertical"
+        android:layout_marginLeft="8dip"
+        android:layout_marginRight="8dip"
+        android:layout_marginBottom="8dip"
+        android:layout_marginTop="8dip"
+        android:lines="2"
+        android:hint="@string/research_feedback_hint"
+        android:inputType="textMultiLine"
+        android:imeOptions="flagNoFullscreen"
+        android:focusable="true"
+    >
+        <requestFocus />
+    </EditText>
+
+    <CheckBox
+        android:id="@+id/research_feedback_include_history"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_marginBottom="8dip"
+        android:checked="true"
+        android:text="@string/research_feedback_include_history_label"
+    />
+</LinearLayout>
diff --git a/java/res/raw/main_en.dict b/java/res/raw/main_en.dict
index 98a9361..e02e300 100644
--- a/java/res/raw/main_en.dict
+++ b/java/res/raw/main_en.dict
Binary files differ
diff --git a/java/res/raw/main_fr.dict b/java/res/raw/main_fr.dict
index 717078c..8e61659 100644
--- a/java/res/raw/main_fr.dict
+++ b/java/res/raw/main_fr.dict
Binary files differ
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-af/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-af/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-af/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-af/strings-appname.xml b/java/res/values-af/strings-appname.xml
new file mode 100644
index 0000000..d6bb52f
--- /dev/null
+++ b/java/res/values-af/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android-sleutelbord"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android-speltoetser"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android-sleutelbordinstellings"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Speltoets tans instellings"</string>
+</resources>
diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml
index 7431fce..332b855 100644
--- a/java/res/values-af/strings.xml
+++ b/java/res/values-af/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android-sleutelbord"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-sleutelbord (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android-sleutelbordinstellings"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Invoeropsies"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-speltoetser"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Navorsing-loglêerbevele"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-speltoetser (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Speltoetser se instellings"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Soek kontakname op"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Speltoetser gebruik inskrywings uit jou kontaklys"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibreer met sleuteldruk"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opsies vir kundiges"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Skakel oor na die ander invoermetodes"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Taal-wisselsleutel dek ook ander invoermetodes"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Onderdruk taal-wisselsleutel"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Sleutelopspringer-wagperiode"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Geen wagperiode nie"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Verstek"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Stel kontakname voor"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Gebruik name van kontakte vir voorstelle en korreksies"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Aktiveer herkorrigerings"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Stel voorstelle vir herkorrigerings"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Outohoofletters"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Voeg woordeboeke by"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Hoofwoordeboek"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Matig"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Aggressief"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Baie aggressief"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Volgendewoordvoorstelle"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Gebruik vorige woord om voorstelle te verbeter"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Volgendewoordvoorspelling"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Gebruik vorige woord ook vir voorspelling"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Stel volgende woord voor"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Gebaseer op vorige woord"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Gebaarinvoer"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Voer \'n woord in deur die letters van \'n woord te trek"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Wys gebaarspoor"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Wys gebaar se woord"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Wys swewende voorskouwoord saam met die gebaar"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Gestoor"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Gaan"</string>
     <string name="label_next_key" msgid="362972844525672568">"Volgende"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Soek"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punt"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Verander taal"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Volgende"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Vorige"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift geaktiveer"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Kasslot geaktiveer"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift gedeaktiveer"</string>
diff --git a/java/res/values-am/strings-appname.xml b/java/res/values-am/strings-appname.xml
new file mode 100644
index 0000000..fd93114
--- /dev/null
+++ b/java/res/values-am/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"የAndroid ቁልፍ ሰሌዳ"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android የፊደል አራሚ"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android የቁልፍ ሰሌዳ ቅንብሮች"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"የፊደል አራሚ ቅንብሮች"</string>
+</resources>
diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml
index d70c05d..513c176 100644
--- a/java/res/values-am/strings.xml
+++ b/java/res/values-am/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"የAndroid ቁልፍሰሌዳ"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"የAndroid ቁልፍ ሰሌዳ (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"የAndroid ቁልፍሰሌዳ ቅንብሮች"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"ግቤት አማራጮች"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android የፊደል ማረሚያ"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"የጥናት የምዝግብ ማስታወሻ ትዕዛዞች"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android የፊደል ማረሚያ (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"የፊደል አራሚ ቅንብሮች"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"የእውቅያ ስሞችን ተመልከት"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"ፊደል አራሚ ከእውቅያ ዝርዝርህ የገቡትን ይጠቀማል"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"በቁልፍመጫንጊዜ አንዝር"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"ለብቁ ተጠቃሚዎች አማራጮች"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"ወደ ሌሎች የግቤት ስልቶች ቀይር"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"የቋንቋ መቀየሪያ ቁልፍ ሌሎች የግቤት ስልቶችንም ይሸፍናል"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"የቋንቋ መቀየሪያ ቁልፍ አፍን"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"የቁልፍ ብቅ ባይ መዘግየትን ያስወገዳል"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"የዘገየ የለም"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"ነባሪ"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"የዕውቂያ ስም ጠቁም"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"ከዕውቂያዎች ለጥቆማዎች እና ማስተካከያዎች ስሞች ተጠቀም"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"ድጋሚ ለማስተካከል አንቃ"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"ድጋሚ ለማስተካከል ጥቆማዎችን አዘጋጅ"</string>
     <string name="auto_cap" msgid="1719746674854628252">"ራስ-ሰር አቢይ ማድረግ"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"መዝገበ ቃላቶች ጨምር"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"ዋና መዝገበ ቃላት"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"መጠነኛ"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"ኃይለኛ"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"በጣም ቁጡ"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"የቀጣይ ቃል አስተያየቶች"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"ምክሮችን ለማሻሻል ቀዳሚ ቃል ተጠቀም"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"የቀጣይ ቃል ግምት"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"ለትንበያ የቀደመ ቃል እንዲሁ ተጠቀም"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"የቀጣይ ቃል አስተያየቶች"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"በቀዳሚው ቃል ላይ የተመሠረተ"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"የእጅ ምልክት ግብዓት"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"የአንድ ቃል ፊደሎችን በመከታተል አንድ ቃል አስገባ"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"ምልክት የሚሄድበት መንገድ አሳይ"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"የምልክት ቃል አሳይ"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"ተንሳፋፊ የቅድመ እይታ ቃል ከምልክት ጋር አሳይ"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : ተቀምጧል"</string>
     <string name="label_go_key" msgid="1635148082137219148">"ሂድ"</string>
     <string name="label_next_key" msgid="362972844525672568">"በመቀጠል"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"ተመለስ"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"ፍለጋ"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"ነጥብ"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"ቋንቋ ቀይር"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"ቀጣይ"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"ቀዳሚ"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"ቅያር ቁልፍ ነቅቷል"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"አቢያት ማድረጊያ ነቅቷል"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"ቅያር ተሰናክሏል"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-ar/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-ar/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-ar/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-ar/strings-appname.xml b/java/res/values-ar/strings-appname.xml
new file mode 100644
index 0000000..3d81e5d
--- /dev/null
+++ b/java/res/values-ar/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"لوحة مفاتيح Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"التدقيق الإملائي في Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"إعدادات لوحة مفاتيح Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"إعدادات التدقيق الإملائي"</string>
+</resources>
diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml
index 5678c40..b1c7afe 100644
--- a/java/res/values-ar/strings.xml
+++ b/java/res/values-ar/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"لوحة مفاتيح Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"لوحة مفاتيح Android ‏(AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"إعدادات لوحة مفاتيح Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"خيارات الإرسال"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"التدقيق الإملائي في Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"أوامر سجلات البحث"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"التدقيق الإملائي في Android‏ (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"إعدادات التدقيق الإملائي"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"بحث في أسماء جهات الاتصال"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"يستخدم المدقق الإملائي إدخالات من قائمة جهات الاتصال"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"اهتزاز عند ضغط مفتاح"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"خيارات للخبراء"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"التبديل إلى أسلوب إدخال آخر"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"يغطي مفتاح تبديل اللغات أساليب الإدخال الأخرى أيضًا"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"إيقاف مفتاح تبديل اللغات"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"تأخير إزالة النافذة المنبثقة الأساسية"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"بلا تأخير"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"افتراضي"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"اقتراح أسماء جهات الاتصال"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"استخدام الأسماء من جهات الاتصال للاقتراحات والتصحيحات"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"تمكين عمليات إعادة التصحيح"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"تعيين اقتراحات لعمليات إعادة التصحيح"</string>
     <string name="auto_cap" msgid="1719746674854628252">"أحرف كبيرة تلقائيًا"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"القواميس الإضافية"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"القاموس الرئيسي"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"معتدل"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"حاد"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"شديد الصرامة"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"اقتراحات الكلمات التالية"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"استخدام الكلمة السابقة لتحسين الاقتراحات"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"تنبؤ الكلمات التالية"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"استخدام الكلمة السابقة أيضًا للتنبؤ"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"اقتراحات الكلمات التالية"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"استنادًا إلى الكلمة السابقة"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"إدخال الإيماءة"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"يمكنك إدخال كلمة من خلال تتبع أحرف كلمة ما"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"عرض مسار الإيماءة"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"عرض كلمة الإيماءة"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"عرض كلمة معاينة متحركة مع الإيماءة"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : تم الحفظ"</string>
     <string name="label_go_key" msgid="1635148082137219148">"تنفيذ"</string>
     <string name="label_next_key" msgid="362972844525672568">"التالي"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"رجوع"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"بحث"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"نقطة"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"تبديل اللغة"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"التالي"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"السابق"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"تم تمكين Shift"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"تم تمكين Caps lock"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"تم تعطيل Shift"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-be/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-be/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-be/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-be/strings-appname.xml b/java/res/values-be/strings-appname.xml
new file mode 100644
index 0000000..e0aadfa
--- /dev/null
+++ b/java/res/values-be/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Клавіятура Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Iнструмент праверкi правапiсу для Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Налады клавіятуры Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Налады праверкі арфаграфіі"</string>
+</resources>
diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml
index 4d14565..976dccb 100644
--- a/java/res/values-be/strings.xml
+++ b/java/res/values-be/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Клавіятура Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавіятура Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Налады клавіятуры Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Параметры ўводу"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Iнструмент праверкi правапiсу для Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Каманды гiсторыя даследаванняў"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Iнструмент праверкi правапiсу для Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Налады праверкі арфаграфіі"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Шукаць імёны кантактаў"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Модуль праверкі правапісу выкарыстоўвае запісы са спісу кантактаў"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Вібрацыя пры націску клавіш"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Функцыi для спецыялістаў"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Перакл. да інш. спос. ув."</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Кнопка пераключэння мовы звязана i з iншымi спосабамi ўводу"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Забаранiць кнопку пераключэння мовы"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Затрым. скр. падк. клав."</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Няма затрымкі"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Па змаўчанні"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Прапан. імёны кантактаў"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Выкарыстоўваць імёны са спісу кантактаў для прапаноў і выпраўл."</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Уключыць карэкцiроўкі"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Задаць прапановы для карэкцiроўкі"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Аўтаматычна рабіць вялікія літары"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Дадатковыя слоўнікі"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Асноўны слоўнік"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Сціплы"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Агрэсіўны"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Вельмі агрэсіўны"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Падказкi для наступнага слова"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Выкарыстаць папярэдняе слова, каб палепшыць прапановы"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Падказка наступнага слова"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Выкарыстанне папярэдняга слова для падказак"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Падказкi для наступнага слова"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"На аснове папярэдняга слова"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Уваход жэстам"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Увядзiце слова, адсочваючы лiтары"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Паказаць след жэста"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Паказаць слова жэста"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Паказаць плаваючы прагляд слова з жэстам"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Захаваныя"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Пачаць"</string>
     <string name="label_next_key" msgid="362972844525672568">"Далей"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Увод"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Пошук"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Кропка"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Пераключыць мову"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Далей"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Назад"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift уключаны"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock уключаны"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift адключаны"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-bg/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-bg/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-bg/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-bg/strings-appname.xml b/java/res/values-bg/strings-appname.xml
new file mode 100644
index 0000000..49e301d
--- /dev/null
+++ b/java/res/values-bg/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Клавиатура на Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Програма за правописна проверка за Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Настройки на клавиатурата на Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Настройки за проверка на правописа"</string>
+</resources>
diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml
index 106a918..81f72b8 100644
--- a/java/res/values-bg/strings.xml
+++ b/java/res/values-bg/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Клавиатура на Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавиатура на Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Настройки на клавиатурата на Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Опции за въвеждане"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Програма за правописна проверка за Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Команди за рег. файл за проучвания"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Програма за правописна проверка за Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Настройки за проверка на правописа"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Търсене на имена"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"За проверка на правописа се ползват записи от списъка с контакти"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Да вибрира при натискане на клавиш"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Опции за експерти"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Други методи за въвеждане"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Клавишът за превкл. на езика обхваща и други методи за въвеждане"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Потискане"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Отхвърляне на подсказката"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Без задържане"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"По подразбиране"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Предложения за контакти"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Използване на имена от „Контакти“ за предложения и поправки"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Повторни поправки: Актив."</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Задаване на предложения за повторни поправки"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Автоматично поставяне на главни букви"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Добавени речници"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Основен речник"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Умерено"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Агресивно"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Много агресивно"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Предложения за следващата дума"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Използване на предишната дума за подобряване на предложенията"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Предвиждане на следващата дума"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Използване на предишната дума и за предвиждане"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Предложения за следващата дума"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Въз основа на предишната дума"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Въвеждане чрез жест"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Въвеждане на дума чрез проследяване на буквите й"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Следа на жестовете: Показване"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Показване на дума при жестове"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Показване на дума с плаваща визуализация при жест"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Запазено"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Старт"</string>
     <string name="label_next_key" msgid="362972844525672568">"Напред"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Търсене"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Точка"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Смяна на езика"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Следващ"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Предишен"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"„Shift“ е активиран"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"„Caps Lock“ е активиран"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"„Shift“ е деактивиран"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-ca/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-ca/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-ca/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-ca/strings-appname.xml b/java/res/values-ca/strings-appname.xml
new file mode 100644
index 0000000..add5c3f
--- /dev/null
+++ b/java/res/values-ca/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Teclat Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Corrector ortogràfic d\'Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Configuració del teclat d\'Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Configuració de la correcció ortogràfica"</string>
+</resources>
diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml
index e3adbcf..4e66af5 100644
--- a/java/res/values-ca/strings.xml
+++ b/java/res/values-ca/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Teclat Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclat d\'Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Configuració del teclat d\'Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opcions d\'entrada"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Corrector ortogràfic d\'Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Recerca d\'ordres de reg."</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corrector ortogràfic d\'Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Configuració de la correcció ortogràfica"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Cerca noms de contactes"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"El corrector ortogràfic utilitza entrades de la llista de cont."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibra en prémer tecles"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opcions per a experts"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Canvia mètode d\'entrada"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"La tecla de canvi d\'idioma també cobreix altres mètodes d\'entrada"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Suprimeix tecla d\'idioma"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Retard d\'om. em. de tecla"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Sense retard"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Predeterminat"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Suggereix noms contactes"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Utilitza els noms de Contactes per a suggeriments i correccions"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Activa la capacitat de tornar a corregir"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Estableix suggeriments per tornar a corregir"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Majúscules automàtiques"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Diccionaris complementaris"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Diccionari principal"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Moderada"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Total"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Molt agressiu"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Suggeriments de paraula següent"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Utilitza la paraula anterior per millorar els suggeriments"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Predicció de paraula següent"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Utilitza també la paraula anterior per a la predicció"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Suggeriments de paraula següent"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"En funció de la paraula anterior"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Entrada de gestos"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Dibuixa les lletres d\'una paraula per escriure-la"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Mostra el recorregut del gest"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Mostra paraules en fer gestos"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Mostra la paraula de visualització prèvia flotant en fer gestos"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: desada"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Vés"</string>
     <string name="label_next_key" msgid="362972844525672568">"Següent"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Retorn"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Cerca"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punt"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Canvia l\'idioma"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Següent"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Anterior"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Maj activat"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Bloq Maj activat"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Maj desactivat"</string>
diff --git a/java/res/values-cs/bools.xml b/java/res/values-cs/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-cs/bools.xml
+++ b/java/res/values-cs/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
diff --git a/java/res/values-cs/strings-appname.xml b/java/res/values-cs/strings-appname.xml
new file mode 100644
index 0000000..0eeac88
--- /dev/null
+++ b/java/res/values-cs/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Klávesnice Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Kontrola pravopisu Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Nastavení klávesnice Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Nastavení kontroly pravopisu"</string>
+</resources>
diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml
index 90d840c..32c4975 100644
--- a/java/res/values-cs/strings.xml
+++ b/java/res/values-cs/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Klávesnice Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Klávesnice Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Nastavení klávesnice Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Možnosti zadávání textu a dat"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Kontrola pravopisu Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Příkazy vývoj. protokolu"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Kontrola pravopisu Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Nastavení kontroly pravopisu"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Vyhledat kontakty"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Kontrola pravopisu používá záznamy z vašeho seznamu kontaktů."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Při stisku klávesy vibrovat"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Možnosti pro odborníky"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Přepínat metody zadávání"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Klávesa pro přepínání jazyka ovládá i další metody zadávání"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Zakázat kl. přepínání jazyka"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Prodleva vysk. okna klávesnice"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Bez prodlevy"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Výchozí"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Navrhovat jména kontaktů"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Použít jména ze seznamu kontaktů k návrhům a opravám"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Povolit opětovné opravy"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Nastavit návrhy pro opětovné opravy"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Velká písmena automaticky"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Doplňkové slovníky"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Hlavní slovník"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Mírné"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresivní"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Velmi agresivní"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Návrh dalšího slova"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Použít předchozí slovo ke zlepšení návrhů"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Odhad dalšího slova"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Použít předchozí slovo také pro odhad"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Návrhy dalšího slova"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Na základě předchozího slova"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Zadávání gesty"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Napište slovo zadáním jeho písmen."</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Zobrazovat stopu gesta"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Zobrazovat slovo gesta"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Zobrazovat plovoucí náhled slova gesta"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: Uloženo"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Přejít"</string>
     <string name="label_next_key" msgid="362972844525672568">"Další"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"vyhledávací tlačítko"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Tečka"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Přepnout jazyk"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Další"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Předchozí"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Klávesa Shift je aktivní"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Klávesa Caps Lock je aktivní"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Klávesa Shift je neaktivní"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-da/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-da/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-da/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-da/strings-appname.xml b/java/res/values-da/strings-appname.xml
new file mode 100644
index 0000000..faef582
--- /dev/null
+++ b/java/res/values-da/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android-tastatur"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android-stavekontrol"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Indstillinger for Android-tastatur"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Indstillinger for stavekontrol"</string>
+</resources>
diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml
index 50b0b0a..968df5a 100644
--- a/java/res/values-da/strings.xml
+++ b/java/res/values-da/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android-tastatur"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-tastatur (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android-tastatur-indstillinger"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Indstillinger for input"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-stavekontrol"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Forskningslogkommandoer"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-stavekontrol (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Indstillinger for stavekontrol"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Slå kontaktnavne op"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Stavekontrollen bruger poster fra listen over kontaktpersoner"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibration ved tastetryk"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Muligheder for eksperter"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Skift inputmetode"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Tasten til sprogskift gælder også for andre inputmetoder"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Ignorer sprogskifttast"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Forsink. afvis. af taste-pop op"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Ingen forsink."</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Standard"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Foreslå navne på kontakter"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Brug navne fra Kontaktpersoner til forslag og rettelser"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Aktivér fornyet rettelse"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Angiv forslag til fornyet rettelse"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Skriv aut. med stort"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Tillægsordbøger"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Hovedordbog"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Moderat"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Aggressiv"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Meget aggressiv"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Forslag til næste ord"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Brug forrige ord til at forbedre forslag"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Forudsigelse af næste ord"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Brug også tidligere ord til forudsigelse"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Forslag til næste ord"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Baseret på tidligere ord"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Berøringsindtastning"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Indtast et ord ved at skrive bogstaverne for et ord med fingeren"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Vis spor af berøring"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Vis berøringsord"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Vis flydende eksempelord under berøring"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: Gemt"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Gå"</string>
     <string name="label_next_key" msgid="362972844525672568">"Næste"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Tilbage"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Søg"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punktum"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Skift sprog"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Næste"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Forrige"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Skift er aktiveret"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock er aktiveret"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Skift er deaktiveret"</string>
diff --git a/java/res/values-de/bools.xml b/java/res/values-de/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-de/bools.xml
+++ b/java/res/values-de/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
diff --git a/java/res/values-de/strings-appname.xml b/java/res/values-de/strings-appname.xml
new file mode 100644
index 0000000..fc5fb89
--- /dev/null
+++ b/java/res/values-de/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android-Tastatur"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android-Rechtschreibprüfung"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android-Tastatureinstellungen"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Einstellungen für Rechtschreibprüfung"</string>
+</resources>
diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml
index 216aac5..c948ce6 100644
--- a/java/res/values-de/strings.xml
+++ b/java/res/values-de/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android-Tastatur"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-Tastatur (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android-Tastatureinstellungen"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Eingabeoptionen"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-Rechtschreibprüfung"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Forschungsprotokollbefehle"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-Rechtschreibprüfung (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Einstellungen für Rechtschreibprüfung"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Kontaktnamen prüfen"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Rechtschreibprüfung verwendet Einträge aus Ihrer Kontaktliste."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Bei Tastendruck vibrieren"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Optionen für Experten"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Eingabemethoden wechseln"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Sprachwechseltaste umfasst auch andere Eingabemethoden."</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Sprachwechsel unterdrücken"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Tasten-Pop-up"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Keine Verzögerung"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Standard"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Kontakte vorschlagen"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Namen aus \"Kontakte\" als Vorschläge und Korrekturmöglichkeiten anzeigen"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Korrekturen aktivieren"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Vorschläge für Korrekturen festlegen"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Autom. Groß-/Kleinschr."</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Erweiterte Wörterbücher"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Allgemeines Wörterbuch"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Mäßig"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Stark"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Sehr stark"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Vorschläge für nächstes Wort"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Zur Verbesserung von Vorschlägen vorheriges Wort verwenden"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Vervollständigung für nächstes Wort"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Vorheriges Wort auch für Vervollständigung verwenden"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Vorschläge für nächstes Wort"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Auf Grundlage des vorherigen Wortes"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Bewegungseingabe"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Wort durch Nachzeichnen der Buchstaben eingeben"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Spur der Bewegung anzeigen"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Wort bei Bewegung anzeigen"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Vorgeschlagenes Wort bei Bewegung anzeigen"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: gespeichert"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Los"</string>
     <string name="label_next_key" msgid="362972844525672568">"Weiter"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Eingabe"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Suchen"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Aufzählungspunkt"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Sprache wechseln"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Nächste"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Vorherige"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Umschalttaste aktiviert"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Feststelltaste aktiviert"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Umschalttaste deaktiviert"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-el/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-el/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-el/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-el/strings-appname.xml b/java/res/values-el/strings-appname.xml
new file mode 100644
index 0000000..a199655
--- /dev/null
+++ b/java/res/values-el/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Πληκτρολόγιο Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Ορθογραφικός έλεγχος Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Ρυθμίσεις πληκτρολογίου Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Ρυθμίσεις ορθογραφικού ελέγχου"</string>
+</resources>
diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml
index 486346a..63f6512 100644
--- a/java/res/values-el/strings.xml
+++ b/java/res/values-el/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Πληκτρολόγιο Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Πληκτρολόγιο Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Ρυθμίσεις πληκτρολογίου Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Επιλογές εισόδου"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Ορθογραφικός έλεγχος Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Έρευνα εντολών καταγραφής"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Ορθογραφικός έλεγχος Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Ρυθμίσεις ορθογραφικού ελέγχου"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Αναζήτηση ονομάτων επαφών"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Ο ορθογρ. έλεγχος χρησιμοπ. καταχωρίσεις από τη λίστα επαφών σας"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Δόνηση κατά το πάτημα πλήκτρων"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Επιλογές για έμπειρους χρήστες"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Άλλη μέθοδος εισόδου"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Το κλειδί αλλαγής γλώσσας καλύπτει και άλλες μεθόδους εισόδου"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Κατάργ. κλειδιού γλώσσας"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Χρόνος εξαφ. αναδ. παραθ."</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Χωρίς καθυστέρ."</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Προεπιλογή"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Πρόταση ονομάτων επαφών"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Χρησιμοποιήστε ονόματα από τις Επαφές για προτάσεις και διορθ."</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Ενεργ. επανάλ. διορθώσεων"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Ορισμός προτάσεων για επαναλήψεις διορθώσεων"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Αυτόματη χρήση κεφαλαίων"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Πρόσθετα λεξικά"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Κύριο λεξικό"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Μέτρια"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Υψηλή"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Πολύ επιθετική"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Προτάσεις επόμενων λέξεων"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Χρήση προηγούμενης λέξης για τη βελτίωση προτάσεων"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Πρόβλεψη επόμενης λέξης"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Χρησιμοποιήστε, επίσης, την προηγούμενη λέξη για πρόβλεψη"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Προτάσεις επόμενων λέξεων"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Βάσει προηγούμενης λέξης"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Καταχώριση κίνησης"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Καταχώριση μιας λέξης με εντοπισμό των γραμμάτων μιας λέξης"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Εμφάνιση διαδρομής χειρονομίας"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Εμφάνιση λέξης χειρονομίας"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Εμφάνιση κινούμενης προεπισκόπησης λέξης με χειρονομία"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Αποθηκεύτηκε"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Μετ."</string>
     <string name="label_next_key" msgid="362972844525672568">"Επόμενο"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Πλήκτρο Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Αναζήτηση"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Κουκκίδα"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Αλλαγή γλώσσας"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Επόμενο"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Προηγούμενο"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Το Shift ενεργοποιημένο"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Το Caps lock είναι ενεργοποιημένο"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Το Shift είναι απενεργοποιημένο"</string>
diff --git a/java/res/values-en-rGB/strings-appname.xml b/java/res/values-en-rGB/strings-appname.xml
new file mode 100644
index 0000000..ad9e782
--- /dev/null
+++ b/java/res/values-en-rGB/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android keyboard"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android spell checker"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android keyboard settings"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Spell checking settings"</string>
+</resources>
diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml
index 5020076..08b6bc0 100644
--- a/java/res/values-en-rGB/strings.xml
+++ b/java/res/values-en-rGB/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android keyboard"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android keyboard (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android keyboard settings"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Input options"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android spell checker"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Research Log Commands"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android spell checker (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Spellchecking settings"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Look up contact names"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Spell checker uses entries from your contact list"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrate on key-press"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Options for experts"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Switch to other input methods"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Language switch key also covers other input methods"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Suppress language switch key"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Key pop-up dismiss delay"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"No delay"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Default"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Suggest Contact names"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Use names from Contacts for suggestions and corrections"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Enable recorrections"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Set suggestions for recorrections"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Auto-capitalisation"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Add-on dictionaries"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Main dictionary"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Modest"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Aggressive"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Very aggressive"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Next word suggestions"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Use previous word to improve suggestion"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Next word prediction"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Use previous word also for prediction"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Next word suggestions"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Based on previous word"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Gesture input"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Input a word by tracing the letters of a word"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Show gesture trail"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Show gesture word"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Show floating preview word with gesture"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Saved"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Go"</string>
     <string name="label_next_key" msgid="362972844525672568">"Next"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Search"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Dot"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Switch language"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Next"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Previous"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift enabled"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock enabled"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift disabled"</string>
diff --git a/java/res/values-en/bools.xml b/java/res/values-en/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-en/bools.xml
+++ b/java/res/values-en/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
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/xml/kbd_esperanto.xml b/java/res/values-eo/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-eo/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-eo/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-es-rUS/strings-appname.xml b/java/res/values-es-rUS/strings-appname.xml
new file mode 100644
index 0000000..5f08afb
--- /dev/null
+++ b/java/res/values-es-rUS/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Teclado de Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Corrector ortográfico de Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Configuración de teclado de Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Configuración del corrector ortográfico"</string>
+</resources>
diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml
index 34ad0a4..cdd6592 100644
--- a/java/res/values-es-rUS/strings.xml
+++ b/java/res/values-es-rUS/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Teclado de Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado de Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Configuración de teclado de Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opciones de entrada"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Corrector ortográfico de Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comandos registro invest."</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corrector ortográfico de Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Configuración del corrector ortográfico"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Buscar nombres contactos"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"El corrector ortográfico usa entradas de tu lista de contactos."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrar al pulsar teclas"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opciones para expertos"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Otros métodos de entrada"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"La tecla de cambio de idioma abarca otros métodos de entrada."</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Elim. tecla cambio de idioma"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Retraso en rechazo de alerta de tecla"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Sin demora"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Predeterminada"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Sugerir nombres de contacto"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Usar nombres de los contactos para sugerencias y correcciones"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Activar correcciones"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Establecer sugerencias para realizar correcciones"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Mayúsculas automáticas"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Diccionarios complementarios"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Diccionario principal"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Moderado"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Total"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Muy agresivo"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Sugerencias para la palabra siguiente"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Usar la palabra anterior para mejorar las sugerencias"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Predicción de la palabra siguiente"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Usar la palabra anterior también para predicción."</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Sugerencias de palabra siguiente"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Según la palabra anterior"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Entrada de gestos"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Ingresa una palabra trazando sus letras."</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Mostrar recorrido de gesto"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Mostrar palabra de gesto"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Mostrar palabra de vista previa flotante al realizar un gesto"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: guardada"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Ir"</string>
     <string name="label_next_key" msgid="362972844525672568">"Siguiente"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Volver"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Buscar"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punto"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Cambiar idioma"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Siguiente"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Anterior"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Se activó el modo Mayúscula."</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Se activó el bloqueo de mayúsculas."</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Se desactivó el modo Mayúscula"</string>
diff --git a/java/res/values-es/bools.xml b/java/res/values-es/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-es/bools.xml
+++ b/java/res/values-es/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
diff --git a/java/res/values-es/strings-appname.xml b/java/res/values-es/strings-appname.xml
new file mode 100644
index 0000000..cce9a17
--- /dev/null
+++ b/java/res/values-es/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Teclado de Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Corrector ortográfico de Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Ajustes del teclado de Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Ajustes del corrector ortográfico"</string>
+</resources>
diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml
index 3c60aa6..d441ad9 100644
--- a/java/res/values-es/strings.xml
+++ b/java/res/values-es/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Teclado de Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Ajustes del teclado de Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opciones entrada texto"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Corrector de Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comandos registro investigación"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corrector de Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Ajustes del corrector ortográfico"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Nombres de contactos"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Añadir nombres de tu lista de contactos al corrector"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrar al pulsar tecla"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opciones para expertos"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Otros métodos de entrada"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"La tecla de cambio de idioma sirve también para otros métodos"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Quitar tecla de idioma"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Retraso al ampliar tecla"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Sin retraso"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Predeterminado"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Sugerir contactos"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Utilizar nombres de contactos para sugerencias y correcciones"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Activar nuevas correcciones"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Establecer sugerencias para nuevas correcciones"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Mayúsculas automáticas"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Diccionarios complementarios"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Diccionario principal"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Parcial"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Total"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Muy agresiva"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Sugerir siguiente palabra"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Usar palabra anterior para mejorar las sugerencias"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Predecir siguiente palabra"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Utilizar también la palabra anterior para realizar la predicción"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Sugerir siguiente palabra"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Según la palabra anterior"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Entrada de gestos"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Trazar las letras de una palabra para introducirla"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Mostrar recorrido del gesto"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Mostrar palabra del gesto"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Mostrar la palabra de vista previa flotante al realizar un gesto"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: guardada"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Ir"</string>
     <string name="label_next_key" msgid="362972844525672568">"Sig."</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Tecla Intro"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Buscar"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punto"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Cambiar idioma"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Siguiente"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Anterior"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Mayúsculas habilitadas"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Bloqueo de mayúsculas habilitado"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Mayúsculas inhabilitadas"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-et/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-et/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-et/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-et/strings-appname.xml b/java/res/values-et/strings-appname.xml
new file mode 100644
index 0000000..181d597
--- /dev/null
+++ b/java/res/values-et/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Androidi klaviatuur"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Androidi õigekirjakontroll"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Androidi klaviatuuri seaded"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Õigekirjakontrolli seaded"</string>
+</resources>
diff --git a/java/res/values-et/strings.xml b/java/res/values-et/strings.xml
index 4a592c3..dfedf5e 100644
--- a/java/res/values-et/strings.xml
+++ b/java/res/values-et/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Androidi klaviatuur"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-klaviatuur (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Androidi klaviatuuriseaded"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Sisestusvalikud"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidi õigekirjakontroll"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Uuringulogi käsud"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidi õigekirjakontroll (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Õigekirjakontrolli seaded"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Kontakti nimede kontroll."</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Õigekirjakontroll kasutab teie kontaktisikute loendi sissekandeid"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibreeri klahvivajutusel"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Valikud ekspertidele"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Vaheta sisestusmeetodit"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Keelevahetuse võti hõlmab ka muid sisestusmeetodeid"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Keela keelevahetuse võti"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Hüpiku loobumisviivitus"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Viivituseta"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Vaikeseade"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Soovita kontaktkirjeid"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Kasuta soovitusteks ja parandusteks nimesid kontaktiloendist"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Uute paranduste lubamine"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Soovituste seadmine uute paranduste jaoks"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Automaatne suurtähtede kasutamine"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Pistiksõnaraamatud"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Peamine sõnaraamat"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Mõõdukas"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agressiivne"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Väga agressiivne"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Järgmise sõna soovitused"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Kasuta soovituste täiustamiseks eelmist sõna"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Järgmise sõna ennustus"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Kasuta ennustuseks ka eelmist sõna"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Järgmise sõna soovitused"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Eelmise sõna põhjal"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Liigutusega sisest."</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Sisestage sõna, kirjutades sõna tähed sõrmega"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Näita liigutuse jälge"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Näita liigutuse sõna"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Ujuva sõna eelvaate näitamine koos liigutusega"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : salvestatud"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Mine"</string>
     <string name="label_next_key" msgid="362972844525672568">"Edasi"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Tagasi"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Otsing"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punkt"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Keele vahetamine"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Järgmine"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Eelmine"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Tõstuklahv on lubatud"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Suurtähelukk on lubatud"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Tõstuklahv on keelatud"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-fa/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-fa/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-fa/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-fa/strings-appname.xml b/java/res/values-fa/strings-appname.xml
new file mode 100644
index 0000000..ba2a76f
--- /dev/null
+++ b/java/res/values-fa/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"صفحه‌کلید Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"غلط‌گیر املای Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"تنظیمات صفحه‌کلید Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"تنظیمات غلط‌‌ گیر املا"</string>
+</resources>
diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml
index bc0e85b..b8504f7 100644
--- a/java/res/values-fa/strings.xml
+++ b/java/res/values-fa/strings.xml
@@ -20,51 +20,52 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"صفحه کلید Android"</string>
-    <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"صفحه کلید (Android (AOSP"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"تنظیمات صفحه کلید Android"</string>
-    <string name="english_ime_input_options" msgid="3909945612939668554">"گزینه های ورودی"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"غلط‌گیر املای Android"</string>
+    <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"صفحه‌کلید (Android (AOSP"</string>
+    <string name="english_ime_input_options" msgid="3909945612939668554">"گزینه‌های ورودی"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"فرمان‌های گزارش‌گیری پژوهش"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"غلط‌گیر املای Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"تنظیمات غلط گیری املایی"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"جستجوی نام مخاطبین"</string>
-    <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"غلط‌گیر املا از ورودی‌های لیست مخاطبین شما استفاده میکند"</string>
+    <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"غلط‌گیر املا از ورودی‌های لیست مخاطبین شما استفاده می‌کند"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"لرزش با فشار کلید"</string>
     <string name="sound_on_keypress" msgid="6093592297198243644">"صدا با فشار کلید"</string>
     <string name="popup_on_keypress" msgid="123894815723512944">"بازشدن با فشار کلید"</string>
     <string name="general_category" msgid="1859088467017573195">"کلی"</string>
     <string name="correction_category" msgid="2236750915056607613">"تصحیح متن"</string>
-    <string name="misc_category" msgid="6894192814868233453">"سایر گزینه ها"</string>
+    <string name="misc_category" msgid="6894192814868233453">"سایر گزینه‌ها"</string>
     <string name="advanced_settings" msgid="362895144495591463">"تنظیمات پیشرفته"</string>
     <string name="advanced_settings_summary" msgid="4487980456152830271">"گزینه‌هایی برای حرفه‌ای‌ها"</string>
-    <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"تغییر به دیگر روشهای ورودی"</string>
-    <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"کلید تغییر زبان، سایر ورودیهای زبان را نیز پوشش می‌دهد"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"کلید تغییر زبان را فشار دهید"</string>
+    <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"تغییر به دیگر روش‌های ورودی"</string>
+    <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"کلید تغییر زبان، سایر ورودی‌های زبان را نیز پوشش می‌دهد"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"تأخیر در رد کردن کلید نمایشی"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"بدون تأخیر"</string>
-    <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"پیش فرض"</string>
-    <string name="use_contacts_dict" msgid="4435317977804180815">"پیشنهاد نام های مخاطب"</string>
+    <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"پیش‌فرض"</string>
+    <string name="use_contacts_dict" msgid="4435317977804180815">"پیشنهاد نام‌های مخاطب"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"برای پیشنهاد و تصحیح از نام مخاطبین استفاده شود"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"فعال کردن تصحیح مجدد"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"تنظیم پیشنهادات برای تصحیح مجدد"</string>
-    <string name="auto_cap" msgid="1719746674854628252">"نوشتن با حروف بزرگ خودکار"</string>
+    <string name="auto_cap" msgid="1719746674854628252">"بزرگ‌کردن خودکار حروف"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"فرهنگ‌های لغت افزودنی"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"فرهنگ‌ لغت اصلی"</string>
     <string name="prefs_show_suggestions" msgid="8026799663445531637">"نمایش پیشنهادات تصحیح"</string>
-    <string name="prefs_show_suggestions_summary" msgid="1583132279498502825">"نمایش واژه های پیشنهادی در حین تایپ"</string>
+    <string name="prefs_show_suggestions_summary" msgid="1583132279498502825">"نمایش واژه‌های پیشنهادی در حین تایپ"</string>
     <string name="prefs_suggestion_visibility_show_name" msgid="3219916594067551303">"همیشه نمایش داده شود"</string>
     <string name="prefs_suggestion_visibility_show_only_portrait_name" msgid="3551821800439659812">"نمایش در حالت عمودی"</string>
     <string name="prefs_suggestion_visibility_hide_name" msgid="6309143926422234673">"همیشه پنهان شود"</string>
     <string name="auto_correction" msgid="4979925752001319458">"تصحیح خودکار"</string>
-    <string name="auto_correction_summary" msgid="5625751551134658006">"کلید فاصله و علائم نگارشی به صورت خودکار کلماتی را که غلط تایپ شده اند تصحیح می کنند"</string>
+    <string name="auto_correction_summary" msgid="5625751551134658006">"کلید فاصله و علائم نگارشی به صورت خودکار کلماتی را که غلط تایپ شده‌اند تصحیح می‌کنند"</string>
     <string name="auto_correction_threshold_mode_off" msgid="8470882665417944026">"خاموش"</string>
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"متوسط"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"فعال"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"بسیار پرخاشگرانه"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"پیشنهادات کلمه بعدی"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"برای بهبود پیشنهاد از کلمه قبلی استفاده شود"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"پیش‌بینی کلمه بعدی"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"استفاده از کلمه قبلی برای پیش بینی"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"پیشنهادات کلمه بعدی"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"بر اساس کلمه قبلی"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"ورودی اشاره"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"با دنبال کردن حروف یک کلمه، کلمه را وارد کنید"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"نمایش نسخه آزمایشی حرکت"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"نمایش کلمه در طول ورودی حرکتی"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"نمایش کلمه پیش‌نمایش متحرک با حرکت"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : ذخیره شد"</string>
     <string name="label_go_key" msgid="1635148082137219148">"برو"</string>
     <string name="label_next_key" msgid="362972844525672568">"بعدی"</string>
@@ -99,6 +100,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"جستجو"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"نقطه"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"تغییر زبان"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"بعدی"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"قبلی"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift فعال است"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock فعال شد"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift غیرفعال است"</string>
@@ -107,26 +111,26 @@
     <string name="spoken_description_mode_phone" msgid="6520207943132026264">"حالت تلفن"</string>
     <string name="spoken_description_mode_phone_shift" msgid="5499629753962641227">"حالت نمادهای تلفن"</string>
     <string name="voice_input" msgid="3583258583521397548">"کلید ورودی صدا"</string>
-    <string name="voice_input_modes_main_keyboard" msgid="3360660341121083174">"در صفحه کلید اصلی"</string>
-    <string name="voice_input_modes_symbols_keyboard" msgid="7203213240786084067">"در صفحه کلید نمادها"</string>
+    <string name="voice_input_modes_main_keyboard" msgid="3360660341121083174">"در صفحه‌کلید اصلی"</string>
+    <string name="voice_input_modes_symbols_keyboard" msgid="7203213240786084067">"در صفحه‌کلید نمادها"</string>
     <string name="voice_input_modes_off" msgid="3745699748218082014">"خاموش"</string>
-    <string name="voice_input_modes_summary_main_keyboard" msgid="6586544292900314339">"میکروفن در صفحه کلید اصلی"</string>
-    <string name="voice_input_modes_summary_symbols_keyboard" msgid="5233725927281932391">"میکروفن در صفحه کلید نمادها"</string>
+    <string name="voice_input_modes_summary_main_keyboard" msgid="6586544292900314339">"میکروفن در صفحه‌کلید اصلی"</string>
+    <string name="voice_input_modes_summary_symbols_keyboard" msgid="5233725927281932391">"میکروفن در صفحه‌کلید نمادها"</string>
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"ورودی صدا غیرفعال است"</string>
-    <string name="configure_input_method" msgid="373356270290742459">"پیکربندی روش های ورودی"</string>
-    <string name="language_selection_title" msgid="1651299598555326750">"زبان های ورودی"</string>
+    <string name="configure_input_method" msgid="373356270290742459">"پیکربندی روش‌های ورودی"</string>
+    <string name="language_selection_title" msgid="1651299598555326750">"زبان‌های ورودی"</string>
     <string name="select_language" msgid="3693815588777926848">"زبان‌های ورودی"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"برای ذخیره دوباره لمس کنید"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"دیکشنری موجود است"</string>
     <string name="prefs_enable_log" msgid="6620424505072963557">"فعال کردن بازخورد کاربر"</string>
-    <string name="prefs_description_log" msgid="5827825607258246003">"با ارسال خودکار آمارهای کاربرد و گزارش های خرابی به Google، به بهبود این ویرایشگر روش ورودی کمک کنید."</string>
-    <string name="keyboard_layout" msgid="8451164783510487501">"طرح زمینه صفحه کلید"</string>
-    <string name="subtype_en_GB" msgid="88170601942311355">"انگیسی (UK)"</string>
-    <string name="subtype_en_US" msgid="6160452336634534239">"انگیسی (US)"</string>
+    <string name="prefs_description_log" msgid="5827825607258246003">"با ارسال خودکار آمارهای کاربرد و گزارش‌های خرابی به Google، به بهبود این ویرایشگر روش ورودی کمک کنید."</string>
+    <string name="keyboard_layout" msgid="8451164783510487501">"طرح زمینه صفحه‌کلید"</string>
+    <string name="subtype_en_GB" msgid="88170601942311355">"انگلیسی (بریتانیا)"</string>
+    <string name="subtype_en_US" msgid="6160452336634534239">"انگلیسی (امریکا)"</string>
     <string name="subtype_with_layout_en_GB" msgid="2179097748724725906">"انگلیسی (انگلستان) (<xliff:g id="LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_en_US" msgid="1362581347576714579">"انگلیسی (ایالات متحده) (<xliff:g id="LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_no_language" msgid="141420857808801746">"زبانی موجود نیست"</string>
-    <string name="subtype_no_language_qwerty" msgid="2956121451616633133">"هیچ کدام از زبانها (QWERTY)"</string>
+    <string name="subtype_no_language_qwerty" msgid="2956121451616633133">"بدون زبان (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="1177848172397202890">"هیچکدام از زبان‌ها (QWERTZ)"</string>
     <string name="subtype_no_language_azerty" msgid="8721460968141187394">"هیچکدام از زبان‌ها (AZERTY)"</string>
     <string name="subtype_no_language_dvorak" msgid="3122976737669823935">"هیچکدام از زبان‌ها (Dvorak)"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-fi/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-fi/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-fi/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-fi/strings-appname.xml b/java/res/values-fi/strings-appname.xml
new file mode 100644
index 0000000..b2e23d5
--- /dev/null
+++ b/java/res/values-fi/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android-näppäimistö"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android-oikoluku"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android-näppäimistön asetukset"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Oikolukuasetukset"</string>
+</resources>
diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml
index 97002ed..68f946c 100644
--- a/java/res/values-fi/strings.xml
+++ b/java/res/values-fi/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android-näppäimistö"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-näppäimistö (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android-näppäimistön asetukset"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Syöttövalinnat"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-oikoluku"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Tutkimuslokin komennot"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-oikoluku (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Oikoluvun asetukset"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Hae kontaktien nimiä"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Oikeinkirjoituksen tarkistus käyttää kontaktiluettelosi tietoja."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Käytä värinää näppäimiä painettaessa"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Valinnat asiantuntijoille"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Käytä toista syöttötapaa"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Kielenvaihtonäppäin kattaa myös muut syöttötavat"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Piilota kielenvaihtonäpp."</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Näppäimen hylkäysviive"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Ei viivettä"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Oletus"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Ehdota yhteystietojen nimiä"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Käytä yhteystietojen nimiä ehdotuksissa ja korjauksissa"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Ota korjaukset käyttöön"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Aseta korjausehdotuksia"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Automaattiset isot kirjaimet"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Lisäsanakirjat"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Pääsanakirja"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Osittainen"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Täysi"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Hyvin aggressiivinen"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Seuraavan sanan ehdotukset"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Paranna ehdotuksia aiempien sanojen avulla"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Seuraavan sanan ennakointi"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Käytä edellistä sanaa myös ennakointiin"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Seuraavan sanan ehdotukset"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Perustuu edelliseen sanan"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Eleiden syöttö"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Syötä sana piirtämällä kirjaimet sormella"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Näytä eleen jälki"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Näytä elesanat"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Näytä eleen yhteydessä kelluva esikatselusana"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Tallennettu"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Siirry"</string>
     <string name="label_next_key" msgid="362972844525672568">"Seur."</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Haku"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Piste"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Vaihda kieli"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Seuraava"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Edellinen"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Vaihto päällä"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock päällä"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Vaihto pois käytöstä"</string>
diff --git a/java/res/values-fr/bools.xml b/java/res/values-fr/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-fr/bools.xml
+++ b/java/res/values-fr/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
diff --git a/java/res/values-fr/donottranslate.xml b/java/res/values-fr/donottranslate.xml
index 8cf2516..5288bd7 100644
--- a/java/res/values-fr/donottranslate.xml
+++ b/java/res/values-fr/donottranslate.xml
@@ -25,5 +25,7 @@
     <!-- Symbols that should promote magic spaces into real space -->
     <string name="phantom_space_promoting_symbols">;:!?([*&amp;@{&lt;&gt;+=|</string>
     <!-- Symbols that do NOT separate words -->
-    <string name="symbols_excluded_from_word_separators">\'</string>
+    <!-- Note that this is identical to the default value, but since the above ones are different
+         and those variables only make sense together, this is kept here for readability. -->
+    <string name="symbols_excluded_from_word_separators">\'-</string>
 </resources>
diff --git a/java/res/values-fr/strings-appname.xml b/java/res/values-fr/strings-appname.xml
new file mode 100644
index 0000000..8e2a6e0
--- /dev/null
+++ b/java/res/values-fr/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Clavier Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Correcteur orthographique Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Paramètres du clavier Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Paramètres du correcteur orthographique"</string>
+</resources>
diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml
index 285d222..b680c57 100644
--- a/java/res/values-fr/strings.xml
+++ b/java/res/values-fr/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Clavier Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Clavier Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Paramètres du clavier Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Options de saisie"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Correcteur orthographique Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Commandes journaux rech."</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Correcteur orthographique Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Paramètre du correcteur orthographique"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Rechercher noms contacts"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Correcteur orthographique utilise entrées de liste de contacts."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrer à chaque touche"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Options destinées aux experts"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Autres modes de saisie"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"La touche de sélection de langue couvre d\'autres modes de saisie."</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Suppr. touche sélect. langue"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Masquer touche agrandie"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Sans délai"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Par défaut"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Proposer noms de contacts"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Utiliser des noms de contacts pour les suggestions et corrections"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Activer la recorrection"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Définir des suggestions de recorrection"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Majuscules auto"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Dictionnaires complémentaires"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Dictionnaire principal"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Simple"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Proactive"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Très exigeante"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Suggestions pour le mot suivant"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Améliorer les suggestions grâce au mot précédent"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Prédiction du mot suivant"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Utiliser le mot précédent pour la prédiction"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Suggestions pour le mot suivant"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Suggestions basées sur le mot précédent"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Saisie gestuelle"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Saisir un mot en traçant ses lettres avec le doigt"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Afficher le tracé du geste"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Afficher un mot lors du geste"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Afficher un aperçu flottant du mot lors de la saisie gestuelle"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : enregistré"</string>
     <string name="label_go_key" msgid="1635148082137219148">"OK"</string>
     <string name="label_next_key" msgid="362972844525672568">"Suiv."</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Entrée"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Rechercher"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Point"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Changer de langue"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Touche suivante"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Touche précédente"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Touche Maj activée"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Verrouillage des majuscules activé"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Touche Maj désactivée"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-hi/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-hi/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-hi/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-hi/strings-appname.xml b/java/res/values-hi/strings-appname.xml
new file mode 100644
index 0000000..02283af
--- /dev/null
+++ b/java/res/values-hi/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android कीबोर्ड"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android वर्तनी परीक्षक"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android कीबोर्ड सेटिंग"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"वर्तनी जांच सेटिंग"</string>
+</resources>
diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml
index 2b18084..8a0f25d 100644
--- a/java/res/values-hi/strings.xml
+++ b/java/res/values-hi/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android कीबोर्ड"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android कीबोर्ड (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android कीबोर्ड सेटिंग"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"इनपुट विकल्‍प"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android वर्तनी परीक्षक"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"लॉग आदेशों का शोध करें"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android वर्तनी परीक्षक (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"वर्तनी जांच सेटिंग"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"संपर्क नामों को खोजें"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"वर्तनी परीक्षक आपकी संपर्क सूची की प्रविष्टियों का उपयोग करता है"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"कुंजी दबाने पर कंपन करता है"</string>
@@ -39,21 +36,22 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"विशेषज्ञों के लिए विकल्‍प"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"अन्‍य इनपुट पद्धतियों पर जाएं"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"भाषा स्‍विच कुंजी में अन्‍य इनपुट पद्धतियां भी शामिल हैं"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"भाषा स्‍विच कुंजी रोकें"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"कुंजी पॉपअप खारिज़ विलंब"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"कोई विलंब नहीं"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"डिफ़ॉल्ट"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"संपर्क नाम सुझाएं"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"सुझाव और सुधार के लिए संपर्क से नामों का उपयोग करें"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"पुन: सुधार सक्षम करें"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"पुन: सुधार के लि‍ए सुझाव सेट करें"</string>
     <string name="auto_cap" msgid="1719746674854628252">"स्‍वत: अक्षर बड़े करना"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"एड-ऑन डिक्शनरी"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"मुख्‍य डिक्‍शनरी"</string>
     <string name="prefs_show_suggestions" msgid="8026799663445531637">"सुधार सुझाव दिखाएं"</string>
-    <string name="prefs_show_suggestions_summary" msgid="1583132279498502825">"लिखते समय सुझाए गए शब्‍द प्रदर्शित करें"</string>
+    <string name="prefs_show_suggestions_summary" msgid="1583132279498502825">"लिखते समय सुझाए गए शब्‍द दिखाएं"</string>
     <string name="prefs_suggestion_visibility_show_name" msgid="3219916594067551303">"हमेशा दिखाएं"</string>
-    <string name="prefs_suggestion_visibility_show_only_portrait_name" msgid="3551821800439659812">"र्पोट्रेट मोड पर प्रदर्शित करें"</string>
+    <string name="prefs_suggestion_visibility_show_only_portrait_name" msgid="3551821800439659812">"र्पोट्रेट मोड पर दिखाएं"</string>
     <string name="prefs_suggestion_visibility_hide_name" msgid="6309143926422234673">"हमेशा छुपाएं"</string>
     <string name="auto_correction" msgid="4979925752001319458">"स्‍वत: सुधार"</string>
     <string name="auto_correction_summary" msgid="5625751551134658006">"Spacebar और विराम चिह्न गलत लिखे गए शब्‍दों को स्‍वचालित रूप से ठीक करते हैं"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"साधारण"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"तीव्र"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"बहुत तीव्र"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"अगला शब्‍द सुझाव"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"सुझावों को बेहतर बनाने के लिए पिछले शब्‍द का उपयोग करें"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"अगला शब्‍द पूर्वानुमान"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"पूर्वानुमान के लिए पिछले शब्द का उपयोग करें"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"अगले शब्द सुझाव"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"पिछले शब्द के आधार पर"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"जेस्‍चर इनपुट"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"किसी शब्द के अक्षरों को ट्रेस करके कोई शब्द इनपुट करें"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"जेस्चर ट्रेल दिखाएं"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"जेस्चर शब्द दिखाएं"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"जेस्चर के साथ फ़्लोटिंग पूर्वावलोकन शब्द दिखाएं"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: सहेजा गया"</string>
     <string name="label_go_key" msgid="1635148082137219148">"जाएं"</string>
     <string name="label_next_key" msgid="362972844525672568">"अगला"</string>
@@ -74,7 +75,7 @@
     <string name="label_to_alpha_key" msgid="4793983863798817523">"कखग"</string>
     <string name="label_to_symbol_key" msgid="8516904117128967293">"?१२३"</string>
     <string name="label_to_symbol_with_microphone_key" msgid="9035925553010061906">"१२३"</string>
-    <string name="label_pause_key" msgid="181098308428035340">"रोकें"</string>
+    <string name="label_pause_key" msgid="181098308428035340">"पॉज़ करें"</string>
     <string name="label_wait_key" msgid="6402152600878093134">"प्रतीक्षा करें"</string>
     <string name="spoken_use_headphones" msgid="896961781287283493">"ज़ोर से बोली गई पासवर्ड कुंजियां सुनने के लिए हेडसेट प्‍लग इन करें."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"वर्तमान पाठ %s है"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"रिटर्न"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"खोजें"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"बिंदु"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"भाषा स्विच करें"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"अगला"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"पिछला"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift सक्षम किया गया"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock सक्षम किया गया"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift अक्षम किया गया"</string>
@@ -115,7 +119,7 @@
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"सहेजने के लिए पुन: स्‍पर्श करें"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"शब्‍दकोश उपलब्‍ध है"</string>
     <string name="prefs_enable_log" msgid="6620424505072963557">"उपयोगकर्ता फ़ीडबैक सक्षम करें"</string>
-    <string name="prefs_description_log" msgid="5827825607258246003">"उपयोग के आंकड़े और क्रैश रिपोर्ट Google को स्वचालित रूप से भेज कर इस इनपुट पद्धति संपादक को बेहतर बनाने में सहायता करें."</string>
+    <string name="prefs_description_log" msgid="5827825607258246003">"उपयोग के आंकड़े और क्रैश रिपोर्ट Google को अपने आप भेज कर इस इनपुट पद्धति संपादक को बेहतर बनाने में सहायता करें."</string>
     <string name="keyboard_layout" msgid="8451164783510487501">"कीबोर्ड थीम"</string>
     <string name="subtype_en_GB" msgid="88170601942311355">"अंग्रेज़ी (यूके)"</string>
     <string name="subtype_en_US" msgid="6160452336634534239">"अंग्रेज़ी (यूएस)"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-hr/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-hr/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-hr/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-hr/strings-appname.xml b/java/res/values-hr/strings-appname.xml
new file mode 100644
index 0000000..69fa2e9
--- /dev/null
+++ b/java/res/values-hr/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Androidova tipkovnica"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Androidova provjera pravopisa"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Postavke Androidove tipkovnice"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Postavke provjere pravopisa"</string>
+</resources>
diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml
index a59d469..f92d23e 100644
--- a/java/res/values-hr/strings.xml
+++ b/java/res/values-hr/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android tipkovnica"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android tipkovnica (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Postavke tipkovnice za Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opcije ulaza"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidova provjera pravopisa"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Istraživanje naredbi dnevnika"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidova provjera pravopisa (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Postavke provjere pravopisa"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Potražite imena kontakata"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Provjera pravopisa upotrebljava unose iz vašeg popisa kontakata"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibracija pri pritisku na tipku"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opcije za stručnjake"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Prebaci na druge unose"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Tipka za prebacivanje jezika pokriva i druge načine unosa"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Spriječi tipku za jezike"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Odgoda prikaza tipki"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Bez odgode"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Zadano"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Predlaži imena kontakata"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Upotreba imena iz Kontakata za prijedloge i ispravke"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Omogući ponovne ispravke"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Postavite prijedloge za ponovne ispravke"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Automatsko pisanje velikih slova"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Rječnici-dodaci"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Glavni rječnik"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Skromno"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresivno"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Vrlo agresivno"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Prijedlozi za sljedeću riječ"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Upotrijebi prethodnu riječ radi poboljšanja prijedloga"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Predviđanje sljedeće riječi"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Upotrijebite prethodnu riječ i za predviđanje"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Prijedlozi za sljedeću riječ"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Na temelju prethodne riječi"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Unos pokretom"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Unos riječi ispisivanjem slova riječi"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Prikaži trag pokreta"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Prikaži riječ pokreta"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Prikaži lebdeći pregled riječi uz pokret"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Spremljeno"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Idi"</string>
     <string name="label_next_key" msgid="362972844525672568">"Dalje"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Pretraživanje"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Točka"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Promijeni jezik"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Sljedeće"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Prethodno"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Omogućena tipka Shift"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Omogućeno pisanje velikih slova"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Onemogućena tipka Shift"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-hu/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-hu/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-hu/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-hu/strings-appname.xml b/java/res/values-hu/strings-appname.xml
new file mode 100644
index 0000000..ad511cf
--- /dev/null
+++ b/java/res/values-hu/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android-billentyűzet"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Androidos helyesírás-ellenőrző"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android-billentyűzet beállításai"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"A helyesírás-ellenőrzés beállításai"</string>
+</resources>
diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml
index 0eac1a9..2073e79 100644
--- a/java/res/values-hu/strings.xml
+++ b/java/res/values-hu/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android-billentyűzet"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-billentyűzet (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android billentyűzetbeállítások"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Beviteli beállítások"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidos helyesírás-ellenőrző"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Naplózási parancsok"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidos helyesírás-ellenőrző (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Helyesírás-ellenőrzés beállításai"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Névjegyek keresése"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"A helyesírás-ellenőrző használja a névjegyek bejegyzéseit"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Rezgés billentyű megnyomása esetén"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Beállítások gyakorlott felhasználóknak"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Váltás más beviteli módra"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"A nyelvkapcsoló gomb egyéb beviteli módokat is tartalmaz"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"A nyelvkapcsoló elrejtése"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Gombeltüntetés késése"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Nincs késés"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Alapbeállítás"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Javasolt névjegyek"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"A névjegyek használata a javaslatokhoz és javításokhoz"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Újbóli javítás engedélyezése"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Javaslatok beállítása az újbóli javításokhoz"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Automatikusan nagy kezdőbetű"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Bővítmények: szótárak"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Fő szótár"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Mérsékelt"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresszív"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Nagyon agresszív"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Következő szóra vonatkozó javaslatok"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Javaslatok fejlesztése az előző szó használatával"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Következő szó előrejelzése"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Az előző szó használata a prediktív bevitelhez is"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Következő szóra vonatkozó javaslatok"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Az előző szó alapján"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Kézi bevitel"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Szó beírása a betűk megrajzolásával"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Mozdulat irányának mutatása"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Mozdulatot leíró szó mutatása"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Mozdulatot leíró szó mutatása lebegő előnézetben"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : mentve"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Ugrás"</string>
     <string name="label_next_key" msgid="362972844525672568">"Tovább"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Keresés"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Pont"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Nyelvek felcserélése"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Következő"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Előző"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift bekapcsolva"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock bekapcsolva"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift kikapcsolva"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-in/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-in/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-in/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-in/strings-appname.xml b/java/res/values-in/strings-appname.xml
new file mode 100644
index 0000000..283d692
--- /dev/null
+++ b/java/res/values-in/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Keyboard Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Pemeriksa ejaan Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Setelan keyboard Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Setelan pemeriksa ejaan"</string>
+</resources>
diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml
index e0e92b5..8b4cd6e 100644
--- a/java/res/values-in/strings.xml
+++ b/java/res/values-in/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Keyboard Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Keyboard Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Setelan keyboard Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opsi masukan"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Pemeriksa ejaan Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Riset Perintah Log"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Pemeriksa ejaan Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Setelan pemeriksaan ejaan"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Cari nama kontak"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Pemeriksa ejaan menggunakan entri dari daftar kontak Anda"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Getar jika tombol ditekan"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opsi untuk ahli"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Beralih ke metode masukan lain"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Tombol beralih bahasa juga mencakup metode masukan lain"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Redam tombol alih bahasa"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Tundaan singkir munculan kunci"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Tanpa penundaan"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Default"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Sarankan nama Kontak"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Menggunakan nama dari Kontak untuk saran dan koreksi"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Aktifkan koreksi ulang"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Setel saran untuk koreksi ulang"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Kapitalisasi otomatis"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Kamus pengaya"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Kamus utama"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Sederhana"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresif"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Sangat agresif"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Saran kata berikutnya"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Gunakan kata sebelumnya untuk meningkatkan saran"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Prediksi kata berikutnya"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Gunakan kata sebelumnya juga untuk prediksi"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Saran kata berikutnya"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Berdasarkan kata sebelumnya"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Masukan isyarat"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Masukkan kata dengan melacak huruf dari sebuah kata"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Tampilkan jalur isyarat"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Tampilkan kata isyarat"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Tampilkan kata pratinjau melayang dengan isyarat"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Telah disimpan"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Buka"</string>
     <string name="label_next_key" msgid="362972844525672568">"Berikutnya"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Kembali"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Telusuri"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Titik"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Ganti bahasa"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Berikutnya"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Sebelumnya"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift diaktifkan"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock diaktifkan"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift dinonaktifkan"</string>
@@ -139,7 +143,7 @@
     <string name="enable" msgid="5031294444630523247">"Aktifkan"</string>
     <string name="not_now" msgid="6172462888202790482">"Nanti saja"</string>
     <string name="custom_input_style_already_exists" msgid="8008728952215449707">"Sudah ada gaya masukan yang sama: <xliff:g id="INPUT_STYLE_NAME">%s</xliff:g>"</string>
-    <string name="prefs_usability_study_mode" msgid="1261130555134595254">"Modus studi daya guna"</string>
+    <string name="prefs_usability_study_mode" msgid="1261130555134595254">"Mode studi daya guna"</string>
     <string name="prefs_keypress_vibration_duration_settings" msgid="1829950405285211668">"Setelan durasi getaran saat tombol ditekan"</string>
     <string name="prefs_keypress_sound_volume_settings" msgid="5875933757082305040">"Setelan volume suara saat tombol ditekan"</string>
 </resources>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-is/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-is/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-is/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-is/strings.xml b/java/res/values-is/strings.xml
new file mode 100644
index 0000000..89be4fc
--- /dev/null
+++ b/java/res/values-is/strings.xml
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for aosp_android_keyboard_ime_name (7877134937939182296) -->
+    <skip />
+    <!-- no translation found for english_ime_input_options (3909945612939668554) -->
+    <skip />
+    <!-- no translation found for english_ime_research_log (8492602295696577851) -->
+    <skip />
+    <!-- no translation found for aosp_spell_checker_service_name (6985142605330377819) -->
+    <skip />
+    <!-- no translation found for use_contacts_for_spellchecking_option_title (5374120998125353898) -->
+    <skip />
+    <!-- no translation found for use_contacts_for_spellchecking_option_summary (8754413382543307713) -->
+    <skip />
+    <!-- no translation found for vibrate_on_keypress (5258079494276955460) -->
+    <skip />
+    <!-- no translation found for sound_on_keypress (6093592297198243644) -->
+    <skip />
+    <!-- no translation found for popup_on_keypress (123894815723512944) -->
+    <skip />
+    <!-- no translation found for general_category (1859088467017573195) -->
+    <skip />
+    <!-- no translation found for correction_category (2236750915056607613) -->
+    <skip />
+    <!-- no translation found for misc_category (6894192814868233453) -->
+    <skip />
+    <!-- no translation found for advanced_settings (362895144495591463) -->
+    <skip />
+    <!-- no translation found for advanced_settings_summary (4487980456152830271) -->
+    <skip />
+    <!-- no translation found for include_other_imes_in_language_switch_list (4533689960308565519) -->
+    <skip />
+    <!-- no translation found for include_other_imes_in_language_switch_list_summary (840637129103317635) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_delay (6213164897443068248) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_no_delay (2096123151571458064) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_default_delay (2166964333903906734) -->
+    <skip />
+    <!-- no translation found for use_contacts_dict (4435317977804180815) -->
+    <skip />
+    <!-- no translation found for use_contacts_dict_summary (6599983334507879959) -->
+    <skip />
+    <!-- no translation found for auto_cap (1719746674854628252) -->
+    <skip />
+    <!-- no translation found for configure_dictionaries_title (4238652338556902049) -->
+    <skip />
+    <!-- no translation found for main_dictionary (4798763781818361168) -->
+    <skip />
+    <!-- no translation found for prefs_show_suggestions (8026799663445531637) -->
+    <skip />
+    <!-- no translation found for prefs_show_suggestions_summary (1583132279498502825) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_show_name (3219916594067551303) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_show_only_portrait_name (3551821800439659812) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_hide_name (6309143926422234673) -->
+    <skip />
+    <!-- no translation found for auto_correction (4979925752001319458) -->
+    <skip />
+    <!-- no translation found for auto_correction_summary (5625751551134658006) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_off (8470882665417944026) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_modest (8788366690620799097) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_aggeressive (3524029103734923819) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_very_aggeressive (3386782235540547678) -->
+    <skip />
+    <!-- no translation found for bigram_prediction (5809665643352206540) -->
+    <skip />
+    <!-- no translation found for bigram_prediction_summary (3253961591626441019) -->
+    <skip />
+    <!-- no translation found for gesture_input (3310827802759290774) -->
+    <skip />
+    <!-- no translation found for gesture_input_summary (7019742443455085809) -->
+    <skip />
+    <!-- no translation found for gesture_preview_trail (3802333369335722221) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text (6859416520117939680) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text_summary (3333754126434989709) -->
+    <skip />
+    <!-- no translation found for added_word (8993883354622484372) -->
+    <skip />
+    <string name="label_go_key" msgid="1635148082137219148">"Áfram"</string>
+    <string name="label_next_key" msgid="362972844525672568">"Næsta"</string>
+    <string name="label_previous_key" msgid="1211868118071386787">"Fyrra"</string>
+    <string name="label_done_key" msgid="2441578748772529288">"Lokið"</string>
+    <string name="label_send_key" msgid="2815056534433717444">"Senda"</string>
+    <string name="label_to_alpha_key" msgid="4793983863798817523">"ABC"</string>
+    <!-- no translation found for label_to_symbol_key (8516904117128967293) -->
+    <skip />
+    <!-- no translation found for label_to_symbol_with_microphone_key (9035925553010061906) -->
+    <skip />
+    <!-- no translation found for label_pause_key (181098308428035340) -->
+    <skip />
+    <!-- no translation found for label_wait_key (6402152600878093134) -->
+    <skip />
+    <!-- no translation found for spoken_use_headphones (896961781287283493) -->
+    <skip />
+    <!-- no translation found for spoken_current_text_is (2485723011272583845) -->
+    <skip />
+    <!-- no translation found for spoken_no_text_entered (7479685225597344496) -->
+    <skip />
+    <!-- no translation found for spoken_description_unknown (3197434010402179157) -->
+    <skip />
+    <!-- no translation found for spoken_description_shift (244197883292549308) -->
+    <skip />
+    <!-- no translation found for spoken_description_shift_shifted (1681877323344195035) -->
+    <skip />
+    <!-- no translation found for spoken_description_caps_lock (3276478269526304432) -->
+    <skip />
+    <!-- no translation found for spoken_description_delete (8740376944276199801) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_symbol (5486340107500448969) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_alpha (23129338819771807) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_numeric (591752092685161732) -->
+    <skip />
+    <!-- no translation found for spoken_description_settings (4627462689603838099) -->
+    <skip />
+    <!-- no translation found for spoken_description_tab (2667716002663482248) -->
+    <skip />
+    <!-- no translation found for spoken_description_space (2582521050049860859) -->
+    <skip />
+    <!-- no translation found for spoken_description_mic (615536748882611950) -->
+    <skip />
+    <!-- no translation found for spoken_description_smiley (2256309826200113918) -->
+    <skip />
+    <!-- no translation found for spoken_description_return (8178083177238315647) -->
+    <skip />
+    <!-- no translation found for spoken_description_search (1247236163755920808) -->
+    <skip />
+    <!-- no translation found for spoken_description_dot (40711082435231673) -->
+    <skip />
+    <!-- no translation found for spoken_description_language_switch (5507091328222331316) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_next (8636078276664150324) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_previous (800872415009336208) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_on (5700440798609574589) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_locked (593175803181701830) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_off (657219998449174808) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_symbol (7183343879909747642) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_alpha (3528307674390156956) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_phone (6520207943132026264) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_phone_shift (5499629753962641227) -->
+    <skip />
+    <!-- no translation found for voice_input (3583258583521397548) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_main_keyboard (3360660341121083174) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_symbols_keyboard (7203213240786084067) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_off (3745699748218082014) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_main_keyboard (6586544292900314339) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_symbols_keyboard (5233725927281932391) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_off (63875609591897607) -->
+    <skip />
+    <!-- no translation found for configure_input_method (373356270290742459) -->
+    <skip />
+    <!-- no translation found for language_selection_title (1651299598555326750) -->
+    <skip />
+    <!-- no translation found for select_language (3693815588777926848) -->
+    <skip />
+    <!-- no translation found for hint_add_to_dictionary (573678656946085380) -->
+    <skip />
+    <!-- no translation found for has_dictionary (6071847973466625007) -->
+    <skip />
+    <!-- no translation found for prefs_enable_log (6620424505072963557) -->
+    <skip />
+    <!-- no translation found for prefs_description_log (5827825607258246003) -->
+    <skip />
+    <!-- no translation found for keyboard_layout (8451164783510487501) -->
+    <skip />
+    <!-- no translation found for subtype_en_GB (88170601942311355) -->
+    <skip />
+    <!-- no translation found for subtype_en_US (6160452336634534239) -->
+    <skip />
+    <!-- no translation found for subtype_with_layout_en_GB (2179097748724725906) -->
+    <skip />
+    <!-- no translation found for subtype_with_layout_en_US (1362581347576714579) -->
+    <skip />
+    <!-- no translation found for subtype_no_language (141420857808801746) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_qwerty (2956121451616633133) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_qwertz (1177848172397202890) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_azerty (8721460968141187394) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_dvorak (3122976737669823935) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_colemak (4205992994906097244) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_pcqwerty (8840928374394180189) -->
+    <skip />
+    <!-- no translation found for custom_input_styles_title (8429952441821251512) -->
+    <skip />
+    <!-- no translation found for add_style (6163126614514489951) -->
+    <skip />
+    <!-- no translation found for add (8299699805688017798) -->
+    <skip />
+    <!-- no translation found for remove (4486081658752944606) -->
+    <skip />
+    <!-- no translation found for save (7646738597196767214) -->
+    <skip />
+    <!-- no translation found for subtype_locale (8576443440738143764) -->
+    <skip />
+    <!-- no translation found for keyboard_layout_set (4309233698194565609) -->
+    <skip />
+    <!-- no translation found for custom_input_style_note_message (8826731320846363423) -->
+    <skip />
+    <!-- no translation found for enable (5031294444630523247) -->
+    <skip />
+    <!-- no translation found for not_now (6172462888202790482) -->
+    <skip />
+    <!-- no translation found for custom_input_style_already_exists (8008728952215449707) -->
+    <skip />
+    <!-- no translation found for prefs_usability_study_mode (1261130555134595254) -->
+    <skip />
+    <!-- no translation found for prefs_keypress_vibration_duration_settings (1829950405285211668) -->
+    <skip />
+    <!-- no translation found for prefs_keypress_sound_volume_settings (5875933757082305040) -->
+    <skip />
+</resources>
diff --git a/java/res/values-it/bools.xml b/java/res/values-it/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-it/bools.xml
+++ b/java/res/values-it/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
diff --git a/java/res/values-it/donottranslate.xml b/java/res/values-it/donottranslate.xml
deleted file mode 100644
index 58e9436..0000000
--- a/java/res/values-it/donottranslate.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2009, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Symbols that do NOT separate words -->
-    <string name="symbols_excluded_from_word_separators"></string>
-</resources>
diff --git a/java/res/values-it/strings-appname.xml b/java/res/values-it/strings-appname.xml
new file mode 100644
index 0000000..b84896b
--- /dev/null
+++ b/java/res/values-it/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Tastiera Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Controllo ortografico Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Impostazioni tastiera Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Impostazioni di controllo ortografico"</string>
+</resources>
diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml
index a54958c..1567c20 100644
--- a/java/res/values-it/strings.xml
+++ b/java/res/values-it/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Tastiera Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Tastiera Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Impostazioni tastiera Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opzioni inserimento"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Controllo ortografico Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Ricerca comandi di log"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Controllo ortografico Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Impostazioni di controllo ortografico"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Cerca in nomi contatti"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"La funzione di controllo ortografico usa voci dell\'elenco contatti"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrazione tasti"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opzioni per esperti"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Altri metodi immissione"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Il tasto per cambiare lingua offre altri metodi di immissione"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Elimina tasto cambio lingua"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Ritardo eliminaz. popup tasto"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Nessun ritardo"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Predefinito"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Suggerisci nomi di contatti"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Utilizza nomi di Contatti per suggerimenti e correzioni"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Attiva nuove correzioni"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Imposta suggerimenti per nuove correzioni"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Maiuscole automatiche"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Dizionari aggiuntivi"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Dizionario principale"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Media"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Massima"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Massima"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Suggerimenti parola successiva"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Usa parola precedente per migliorare suggerimenti"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Previsione parola successiva"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Usa anche la parola precedente per la previsione"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Suggerimenti parola successiva"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"In base alla parola precedente"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Inserimento con gesti"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Inserisci una parola tracciandone le lettere"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Mostra traccia con gesto"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Mostra parola con gesto"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Mostra parola di anteprima floating con gesto"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : parola salvata"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Vai"</string>
     <string name="label_next_key" msgid="362972844525672568">"Avanti"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Invio"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Cerca"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Pallino"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Cambia lingua"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Successivo"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Precedente"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Maiuscolo attivo"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Blocco maiuscole attivo"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Maiuscolo disattivato"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-iw/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-iw/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-iw/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-iw/strings-appname.xml b/java/res/values-iw/strings-appname.xml
new file mode 100644
index 0000000..f3f4b67
--- /dev/null
+++ b/java/res/values-iw/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"מקלדת Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"בודק האיות של Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"הגדרות מקלדת Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"הגדרות בדיקת איות"</string>
+</resources>
diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml
index 323cac1..5b6315b 100644
--- a/java/res/values-iw/strings.xml
+++ b/java/res/values-iw/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"מקלדת Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"מקלדת Android ‏(AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"הגדרות מקלדת של Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"אפשרויות קלט"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"בודק האיות של Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"פקודות יומן מחקר"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"בודק האיות של Android ‏(AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"הגדרות בדיקת איות"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"חפש שמות של אנשי קשר"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"בודק האיות משתמש בערכים מרשימת אנשי הקשר שלך"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"רטט בלחיצה על מקשים"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"אפשרויות למומחים"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"עבור לשיטות קלט אחרות"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"מתג החלפת השפה מכסה גם שיטות קלט אחרות"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"הסתר את מתג החלפת השפה"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"עיכוב סגירת חלון קופץ של מקש"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"ללא עיכוב"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"ברירת מחדל"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"הצע שמות של אנשי קשר"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"השתמש בשמות מרשימת אנשי הקשר עבור הצעות ותיקונים"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"הפוך תיקונים חוזרים לפעילים"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"הגדר הצעות עבור תיקונים חוזרים"</string>
     <string name="auto_cap" msgid="1719746674854628252">"הפיכת אותיות לרישיות באופן אוטומטי"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"הוספת מילונים"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"מילון ראשי"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"מצומצם"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"מחמיר"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"מחמיר מאוד"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"הצעות המילה הבאה"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"השתמש במילה הקודמת כדי לשפר את ההצעות"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"חיזוי המילה הבאה"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"השתמש במילה הקודמת גם עבור חיזוי"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"הצעות המילה הבאה"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"בהתבסס על המילה הקודמת"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"קלט מחווה"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"הזן מילה על ידי החלקת האצבע מאות לאות"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"הצג שובל מחווה"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"הצג מילת מחווה"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"הצג תצוגה מקדימה צפה של המילה בזמן המחווה"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : נשמרה"</string>
     <string name="label_go_key" msgid="1635148082137219148">"בצע"</string>
     <string name="label_next_key" msgid="362972844525672568">"הבא"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"חזור"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"חיפוש"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"נקודה"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"החלף שפה"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"הבא"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"הקודם"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift מופעל"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock מופעל"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift מושבת"</string>
diff --git a/java/res/values-ja/strings-appname.xml b/java/res/values-ja/strings-appname.xml
new file mode 100644
index 0000000..16c1c05
--- /dev/null
+++ b/java/res/values-ja/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Androidキーボード"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Androidスペルチェッカー"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Androidキーボードの設定"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"スペルチェックの設定"</string>
+</resources>
diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml
index 3a86516..95d2307 100644
--- a/java/res/values-ja/strings.xml
+++ b/java/res/values-ja/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Androidキーボード"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Androidキーボード(AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Androidキーボードの設定"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"入力オプション"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Androidスペルチェッカー"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"ログコマンドの検索"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Androidスペルチェッカー(AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"スペルチェックの設定"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"連絡先名の検索"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"スペルチェッカーでは連絡先リストのエントリを使用します"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"キー操作バイブ"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"上級者向けオプション"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"他の入力方法に切り替え"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"言語切り替えキーは他の入力方法にも対応しています"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"言語切り替えキーを非表示"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"キーのポップアップ時間"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"すぐに消去"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"デフォルト"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"候補の連絡先名を表示"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"連絡先の名前を使用して候補表示や自動修正を行います"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"再修正を有効にする"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"再修正の候補を挿入する"</string>
     <string name="auto_cap" msgid="1719746674854628252">"自動大文字変換"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"アドオン辞書"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"メイン辞書"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"中"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"強"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"最も強い"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"次の入力候補"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"直前の単語から入力候補を予測します"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"次の入力候補を予測"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"前の語句も予測に使用"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"次の入力候補"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"前の語句に基づいた入力候補表示"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"ジェスチャー入力"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"単語の文字をトレースして単語を入力"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"ジェスチャートレイルを表示"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"ジェスチャーワードを表示"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"ジェスチャーでプレビューワードをフローティング表示できます"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>:保存しました"</string>
     <string name="label_go_key" msgid="1635148082137219148">"実行"</string>
     <string name="label_next_key" msgid="362972844525672568">"次へ"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"検索"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"中点"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"言語を切り替え"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"次へ"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"前へ"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift有効"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock有効"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift解除"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-ka/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-ka/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-ka/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-ka/strings.xml b/java/res/values-ka/strings.xml
new file mode 100644
index 0000000..08c8635
--- /dev/null
+++ b/java/res/values-ka/strings.xml
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for aosp_android_keyboard_ime_name (7877134937939182296) -->
+    <skip />
+    <!-- no translation found for english_ime_input_options (3909945612939668554) -->
+    <skip />
+    <!-- no translation found for english_ime_research_log (8492602295696577851) -->
+    <skip />
+    <!-- no translation found for aosp_spell_checker_service_name (6985142605330377819) -->
+    <skip />
+    <!-- no translation found for use_contacts_for_spellchecking_option_title (5374120998125353898) -->
+    <skip />
+    <!-- no translation found for use_contacts_for_spellchecking_option_summary (8754413382543307713) -->
+    <skip />
+    <!-- no translation found for vibrate_on_keypress (5258079494276955460) -->
+    <skip />
+    <!-- no translation found for sound_on_keypress (6093592297198243644) -->
+    <skip />
+    <!-- no translation found for popup_on_keypress (123894815723512944) -->
+    <skip />
+    <!-- no translation found for general_category (1859088467017573195) -->
+    <skip />
+    <!-- no translation found for correction_category (2236750915056607613) -->
+    <skip />
+    <!-- no translation found for misc_category (6894192814868233453) -->
+    <skip />
+    <!-- no translation found for advanced_settings (362895144495591463) -->
+    <skip />
+    <!-- no translation found for advanced_settings_summary (4487980456152830271) -->
+    <skip />
+    <!-- no translation found for include_other_imes_in_language_switch_list (4533689960308565519) -->
+    <skip />
+    <!-- no translation found for include_other_imes_in_language_switch_list_summary (840637129103317635) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_delay (6213164897443068248) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_no_delay (2096123151571458064) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_default_delay (2166964333903906734) -->
+    <skip />
+    <!-- no translation found for use_contacts_dict (4435317977804180815) -->
+    <skip />
+    <!-- no translation found for use_contacts_dict_summary (6599983334507879959) -->
+    <skip />
+    <!-- no translation found for auto_cap (1719746674854628252) -->
+    <skip />
+    <!-- no translation found for configure_dictionaries_title (4238652338556902049) -->
+    <skip />
+    <!-- no translation found for main_dictionary (4798763781818361168) -->
+    <skip />
+    <!-- no translation found for prefs_show_suggestions (8026799663445531637) -->
+    <skip />
+    <!-- no translation found for prefs_show_suggestions_summary (1583132279498502825) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_show_name (3219916594067551303) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_show_only_portrait_name (3551821800439659812) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_hide_name (6309143926422234673) -->
+    <skip />
+    <!-- no translation found for auto_correction (4979925752001319458) -->
+    <skip />
+    <!-- no translation found for auto_correction_summary (5625751551134658006) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_off (8470882665417944026) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_modest (8788366690620799097) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_aggeressive (3524029103734923819) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_very_aggeressive (3386782235540547678) -->
+    <skip />
+    <!-- no translation found for bigram_prediction (5809665643352206540) -->
+    <skip />
+    <!-- no translation found for bigram_prediction_summary (3253961591626441019) -->
+    <skip />
+    <!-- no translation found for gesture_input (3310827802759290774) -->
+    <skip />
+    <!-- no translation found for gesture_input_summary (7019742443455085809) -->
+    <skip />
+    <!-- no translation found for gesture_preview_trail (3802333369335722221) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text (6859416520117939680) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text_summary (3333754126434989709) -->
+    <skip />
+    <!-- no translation found for added_word (8993883354622484372) -->
+    <skip />
+    <string name="label_go_key" msgid="1635148082137219148">"გადასვლა"</string>
+    <string name="label_next_key" msgid="362972844525672568">"შემდეგი"</string>
+    <string name="label_previous_key" msgid="1211868118071386787">"წინა"</string>
+    <string name="label_done_key" msgid="2441578748772529288">"შესრულებულია"</string>
+    <string name="label_send_key" msgid="2815056534433717444">"გაგზავნა"</string>
+    <string name="label_to_alpha_key" msgid="4793983863798817523">"ABC"</string>
+    <!-- no translation found for label_to_symbol_key (8516904117128967293) -->
+    <skip />
+    <!-- no translation found for label_to_symbol_with_microphone_key (9035925553010061906) -->
+    <skip />
+    <!-- no translation found for label_pause_key (181098308428035340) -->
+    <skip />
+    <!-- no translation found for label_wait_key (6402152600878093134) -->
+    <skip />
+    <!-- no translation found for spoken_use_headphones (896961781287283493) -->
+    <skip />
+    <!-- no translation found for spoken_current_text_is (2485723011272583845) -->
+    <skip />
+    <!-- no translation found for spoken_no_text_entered (7479685225597344496) -->
+    <skip />
+    <!-- no translation found for spoken_description_unknown (3197434010402179157) -->
+    <skip />
+    <!-- no translation found for spoken_description_shift (244197883292549308) -->
+    <skip />
+    <!-- no translation found for spoken_description_shift_shifted (1681877323344195035) -->
+    <skip />
+    <!-- no translation found for spoken_description_caps_lock (3276478269526304432) -->
+    <skip />
+    <!-- no translation found for spoken_description_delete (8740376944276199801) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_symbol (5486340107500448969) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_alpha (23129338819771807) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_numeric (591752092685161732) -->
+    <skip />
+    <!-- no translation found for spoken_description_settings (4627462689603838099) -->
+    <skip />
+    <!-- no translation found for spoken_description_tab (2667716002663482248) -->
+    <skip />
+    <!-- no translation found for spoken_description_space (2582521050049860859) -->
+    <skip />
+    <!-- no translation found for spoken_description_mic (615536748882611950) -->
+    <skip />
+    <!-- no translation found for spoken_description_smiley (2256309826200113918) -->
+    <skip />
+    <!-- no translation found for spoken_description_return (8178083177238315647) -->
+    <skip />
+    <!-- no translation found for spoken_description_search (1247236163755920808) -->
+    <skip />
+    <!-- no translation found for spoken_description_dot (40711082435231673) -->
+    <skip />
+    <!-- no translation found for spoken_description_language_switch (5507091328222331316) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_next (8636078276664150324) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_previous (800872415009336208) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_on (5700440798609574589) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_locked (593175803181701830) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_off (657219998449174808) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_symbol (7183343879909747642) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_alpha (3528307674390156956) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_phone (6520207943132026264) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_phone_shift (5499629753962641227) -->
+    <skip />
+    <!-- no translation found for voice_input (3583258583521397548) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_main_keyboard (3360660341121083174) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_symbols_keyboard (7203213240786084067) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_off (3745699748218082014) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_main_keyboard (6586544292900314339) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_symbols_keyboard (5233725927281932391) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_off (63875609591897607) -->
+    <skip />
+    <!-- no translation found for configure_input_method (373356270290742459) -->
+    <skip />
+    <!-- no translation found for language_selection_title (1651299598555326750) -->
+    <skip />
+    <!-- no translation found for select_language (3693815588777926848) -->
+    <skip />
+    <!-- no translation found for hint_add_to_dictionary (573678656946085380) -->
+    <skip />
+    <!-- no translation found for has_dictionary (6071847973466625007) -->
+    <skip />
+    <!-- no translation found for prefs_enable_log (6620424505072963557) -->
+    <skip />
+    <!-- no translation found for prefs_description_log (5827825607258246003) -->
+    <skip />
+    <!-- no translation found for keyboard_layout (8451164783510487501) -->
+    <skip />
+    <!-- no translation found for subtype_en_GB (88170601942311355) -->
+    <skip />
+    <!-- no translation found for subtype_en_US (6160452336634534239) -->
+    <skip />
+    <!-- no translation found for subtype_with_layout_en_GB (2179097748724725906) -->
+    <skip />
+    <!-- no translation found for subtype_with_layout_en_US (1362581347576714579) -->
+    <skip />
+    <!-- no translation found for subtype_no_language (141420857808801746) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_qwerty (2956121451616633133) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_qwertz (1177848172397202890) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_azerty (8721460968141187394) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_dvorak (3122976737669823935) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_colemak (4205992994906097244) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_pcqwerty (8840928374394180189) -->
+    <skip />
+    <!-- no translation found for custom_input_styles_title (8429952441821251512) -->
+    <skip />
+    <!-- no translation found for add_style (6163126614514489951) -->
+    <skip />
+    <!-- no translation found for add (8299699805688017798) -->
+    <skip />
+    <!-- no translation found for remove (4486081658752944606) -->
+    <skip />
+    <!-- no translation found for save (7646738597196767214) -->
+    <skip />
+    <!-- no translation found for subtype_locale (8576443440738143764) -->
+    <skip />
+    <!-- no translation found for keyboard_layout_set (4309233698194565609) -->
+    <skip />
+    <!-- no translation found for custom_input_style_note_message (8826731320846363423) -->
+    <skip />
+    <!-- no translation found for enable (5031294444630523247) -->
+    <skip />
+    <!-- no translation found for not_now (6172462888202790482) -->
+    <skip />
+    <!-- no translation found for custom_input_style_already_exists (8008728952215449707) -->
+    <skip />
+    <!-- no translation found for prefs_usability_study_mode (1261130555134595254) -->
+    <skip />
+    <!-- no translation found for prefs_keypress_vibration_duration_settings (1829950405285211668) -->
+    <skip />
+    <!-- no translation found for prefs_keypress_sound_volume_settings (5875933757082305040) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ko/strings-appname.xml b/java/res/values-ko/strings-appname.xml
new file mode 100644
index 0000000..3d7db61
--- /dev/null
+++ b/java/res/values-ko/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android 키보드"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android 맞춤법 검사기"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android 키보드 설정"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"맞춤법 검사 설정"</string>
+</resources>
diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml
index 536f06d..4ef392d 100644
--- a/java/res/values-ko/strings.xml
+++ b/java/res/values-ko/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android 키보드"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android 키보드(AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android 키보드 설정"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"입력 옵션"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android 맞춤법 검사기"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"로그 명령 탐색"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android 맞춤법 검사기(AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"맞춤법 검사 설정"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"연락처 이름 조회"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"맞춤법 검사기가 주소록의 항목을 사용합니다."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"키를 누를 때 진동 발생"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"전문가용 옵션"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"다른 입력 방법으로 전환"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"언어 전환 키가 제공하는 기타 입력 방법"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"언어 전환 키 제거"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"키 팝업 해제 지연"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"지연 없음"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"기본값"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"주소록 이름 활용"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"추천 및 수정에 주소록의 이름 사용"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"재수정 가능 설정"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"재수정 추천어 사전 활성화"</string>
     <string name="auto_cap" msgid="1719746674854628252">"자동 대문자화"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"사전 추가"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"기본 사전"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"약"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"중"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"강"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"다음 추천 검색어"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"이전 단어를 사용하여 추천 검색어 개선"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"다음 예상 검색어"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"이전 단어를 사용하여 예상 검색어 표시"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"다음 검색어 추천"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"이전 단어에 기반한 추천"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"동작 입력"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"한번에 문자를 그려서 단어 입력"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"동작 흔적 표시"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"동작 단어 표시"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"동작에 따라 단어 미리보기 표시"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: 저장됨"</string>
     <string name="label_go_key" msgid="1635148082137219148">"이동"</string>
     <string name="label_next_key" msgid="362972844525672568">"다음"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"리턴 키"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"검색"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"점"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"언어 전환"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"다음"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"이전"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift 사용"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock 사용"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift 사용중지"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-ky/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-ky/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-ky/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-land/dimens.xml b/java/res/values-land/dimens.xml
index 6259725..c78c25f 100644
--- a/java/res/values-land/dimens.xml
+++ b/java/res/values-land/dimens.xml
@@ -1,19 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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.
 */
 -->
@@ -55,6 +55,11 @@
     <fraction name="spacebar_text_ratio">40.000%</fraction>
     <dimen name="key_preview_offset">0.0dp</dimen>
 
+    <!-- For 5-row keyboard -->
+    <fraction name="key_bottom_gap_5row">3.20%p</fraction>
+    <fraction name="key_letter_ratio_5row">78%</fraction>
+    <fraction name="key_uppercase_letter_ratio_5row">48%</fraction>
+
     <dimen name="key_preview_offset_ics">1.6dp</dimen>
     <!-- popup_key_height x -0.5 -->
     <dimen name="more_keys_keyboard_vertical_correction_ics">-22.4dp</dimen>
@@ -68,4 +73,10 @@
     <dimen name="more_keys_keyboard_slide_allowance">53.76dp</dimen>
     <!-- popup_key_height x -1.0 -->
     <dimen name="more_keys_keyboard_vertical_correction">-44.8dp</dimen>
+
+    <!-- Gesture floating preview text parameters -->
+    <dimen name="gesture_floating_preview_text_size">23dp</dimen>
+    <dimen name="gesture_floating_preview_text_offset">54dp</dimen>
+    <dimen name="gesture_floating_preview_horizontal_padding">23dp</dimen>
+    <dimen name="gesture_floating_preview_vertical_padding">15dp</dimen>
 </resources>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-lt/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-lt/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-lt/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-lt/strings-appname.xml b/java/res/values-lt/strings-appname.xml
new file mode 100644
index 0000000..668d275
--- /dev/null
+++ b/java/res/values-lt/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"„Android“ klaviatūra"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"„Android“ rašybos tikrinimo programa"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"„Android“ klaviatūros nustatymai"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Rašybos tikrinimo nustatymai"</string>
+</resources>
diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml
index a0b8fc8..cb70d49 100644
--- a/java/res/values-lt/strings.xml
+++ b/java/res/values-lt/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"„Android“ klaviatūra"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"„Android“ klaviatūra (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"„Android“ klaviatūros nustatymai"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Įvesties parinktys"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"„Android“ rašybos tikrinimo programa"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Tyrinėti žurnalo komandas"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"„Android“ rašybos tikrinimo programa (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Rašybos tikrinimo nustatymai"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Kontaktų vardų paieška"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Rašybos tikrinimo progr. naudoja įrašus, esančius kontaktų sąraše"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibruoti, kai paspaudžiami klavišai"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Parinktys ekspertams"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Perj. į kt. įvesties būd."</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Kalbos perjungimo klavišu taip pat perjungiami įvesties būdai"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Nerodyti klb. keit. klav."</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Pagr. išš. l. atsis. d."</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Be delsos"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Numatytasis"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Siūlyti kontaktų vardus"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Siūlant ir taisant naudoti vardus iš „Kontaktų“"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Įdiegti pakartotinius pataisymus"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Nustatyti pakartotinio pataisymo pasiūlymus"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Automatinis didžiųjų raidžių rašymas"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Papildomi žodynai"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Pagrindinis žodynas"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Vidutinis"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Atkaklus"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Labai agresyviai"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Kito žodžio pasiūlymai"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Naudoti ankstesnį žodį pasiūlymams patobulinti"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Kito žodžio numatymas"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Numatant naudoti ir ankstesnį žodį"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Kito žodžio pasiūlymai"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Pagal ankstesnį žodį"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Įvestis gestais"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Įveskite žodį brėždami jo raides"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Rodyti gestų kelią"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Rodyti gesto žodį"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Rodyti kintantį peržiūros žodį gestu"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: išsaugota"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Pradėti"</string>
     <string name="label_next_key" msgid="362972844525672568">"Kitas"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Grįžti"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Ieškoti"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Taškas"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Keisti kalbą"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Kitas"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Ankstesnis"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Įgalintas antrasis lygis"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Įgalintos didžiosios raidės"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Antrasis lygis išjungtas"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-lv/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-lv/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-lv/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-lv/strings-appname.xml b/java/res/values-lv/strings-appname.xml
new file mode 100644
index 0000000..e5657a2
--- /dev/null
+++ b/java/res/values-lv/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android tastatūra"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android pareizrakstības pārbaudītājs"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android tastatūras iestatījumi"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Pareizrakstības pārbaudes iestatījumi"</string>
+</resources>
diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml
index 93727a8..f33b776 100644
--- a/java/res/values-lv/strings.xml
+++ b/java/res/values-lv/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android tastatūra"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android tastatūra (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android tastatūras iestatījumi"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Ievades opcijas"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android pareizrakstības pārbaudītājs"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Izpētes žurnāla komandas"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android pareizrakstības pārbaudītājs (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Pareizrakstības pārbaudes iestatījumi"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Meklēt kontaktp. vārdus"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Pareizrakst. pārbaudītājs lieto ierakstus no kontaktp. saraksta."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrēt, nospiežot taustiņu"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opcijas ekspertiem"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Pārsl. uz citām iev. met."</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Valodas pārslēgš. taustiņu var lietot arī citām ievades metodēm."</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Atsp. val. pārsl. taust."</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Taust. uzn. loga noraid. aizk."</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Bez aizkaves"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Noklusējums"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Ieteikt kontaktp. vārdus"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Izmantot kontaktpersonu vārdus kā ieteikumus un labojumus"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Iespējot atk. labojumus"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Iestatīt atkārtotu labojumu ieteikumus"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Automātiska lielo burtu lietošana"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Papildinājumu vārdnīcas"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Galvenā vārdnīca"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Mērena"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresīva"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Ļoti radikāla"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Nākamā vārda ieteikumi"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Ieteikumu uzlabošanai izmantot iepriekšējo vārdu"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Nākamā vārda prognozēšana"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Izmantot iepriekšējo vārdu arī prognozēm"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Nākamie vārdu ieteikumi"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Pamatojoties uz iepriekšējo vārdu"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Ievade ar žestu"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Ievadiet vārdu, norādot tā burtus."</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Rādīt žesta pēdas"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Rādīt žesta vārdu"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Rādīt peldošo priekšskatījuma vārdu ar žestu"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: saglabāts"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Sākt"</string>
     <string name="label_next_key" msgid="362972844525672568">"Tālāk"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Ievadīšanas taustiņš"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Meklēt"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punkts"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Mainīt valodu"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Nākamā"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Iepriekšējā"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Pārslēgšanas režīms iespējots"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Burtslēgs iespējots"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Pārslēgšanas režīms atspējots"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-mk/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-mk/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-mk/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-mk/strings.xml b/java/res/values-mk/strings.xml
new file mode 100644
index 0000000..f0a6dbe
--- /dev/null
+++ b/java/res/values-mk/strings.xml
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for aosp_android_keyboard_ime_name (7877134937939182296) -->
+    <skip />
+    <!-- no translation found for english_ime_input_options (3909945612939668554) -->
+    <skip />
+    <!-- no translation found for english_ime_research_log (8492602295696577851) -->
+    <skip />
+    <!-- no translation found for aosp_spell_checker_service_name (6985142605330377819) -->
+    <skip />
+    <!-- no translation found for use_contacts_for_spellchecking_option_title (5374120998125353898) -->
+    <skip />
+    <!-- no translation found for use_contacts_for_spellchecking_option_summary (8754413382543307713) -->
+    <skip />
+    <!-- no translation found for vibrate_on_keypress (5258079494276955460) -->
+    <skip />
+    <!-- no translation found for sound_on_keypress (6093592297198243644) -->
+    <skip />
+    <!-- no translation found for popup_on_keypress (123894815723512944) -->
+    <skip />
+    <!-- no translation found for general_category (1859088467017573195) -->
+    <skip />
+    <!-- no translation found for correction_category (2236750915056607613) -->
+    <skip />
+    <!-- no translation found for misc_category (6894192814868233453) -->
+    <skip />
+    <!-- no translation found for advanced_settings (362895144495591463) -->
+    <skip />
+    <!-- no translation found for advanced_settings_summary (4487980456152830271) -->
+    <skip />
+    <!-- no translation found for include_other_imes_in_language_switch_list (4533689960308565519) -->
+    <skip />
+    <!-- no translation found for include_other_imes_in_language_switch_list_summary (840637129103317635) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_delay (6213164897443068248) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_no_delay (2096123151571458064) -->
+    <skip />
+    <!-- no translation found for key_preview_popup_dismiss_default_delay (2166964333903906734) -->
+    <skip />
+    <!-- no translation found for use_contacts_dict (4435317977804180815) -->
+    <skip />
+    <!-- no translation found for use_contacts_dict_summary (6599983334507879959) -->
+    <skip />
+    <!-- no translation found for auto_cap (1719746674854628252) -->
+    <skip />
+    <!-- no translation found for configure_dictionaries_title (4238652338556902049) -->
+    <skip />
+    <!-- no translation found for main_dictionary (4798763781818361168) -->
+    <skip />
+    <!-- no translation found for prefs_show_suggestions (8026799663445531637) -->
+    <skip />
+    <!-- no translation found for prefs_show_suggestions_summary (1583132279498502825) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_show_name (3219916594067551303) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_show_only_portrait_name (3551821800439659812) -->
+    <skip />
+    <!-- no translation found for prefs_suggestion_visibility_hide_name (6309143926422234673) -->
+    <skip />
+    <!-- no translation found for auto_correction (4979925752001319458) -->
+    <skip />
+    <!-- no translation found for auto_correction_summary (5625751551134658006) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_off (8470882665417944026) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_modest (8788366690620799097) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_aggeressive (3524029103734923819) -->
+    <skip />
+    <!-- no translation found for auto_correction_threshold_mode_very_aggeressive (3386782235540547678) -->
+    <skip />
+    <!-- no translation found for bigram_prediction (5809665643352206540) -->
+    <skip />
+    <!-- no translation found for bigram_prediction_summary (3253961591626441019) -->
+    <skip />
+    <!-- no translation found for gesture_input (3310827802759290774) -->
+    <skip />
+    <!-- no translation found for gesture_input_summary (7019742443455085809) -->
+    <skip />
+    <!-- no translation found for gesture_preview_trail (3802333369335722221) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text (6859416520117939680) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text_summary (3333754126434989709) -->
+    <skip />
+    <!-- no translation found for added_word (8993883354622484372) -->
+    <skip />
+    <string name="label_go_key" msgid="1635148082137219148">"Оди"</string>
+    <string name="label_next_key" msgid="362972844525672568">"Следно"</string>
+    <string name="label_previous_key" msgid="1211868118071386787">"Претходно"</string>
+    <string name="label_done_key" msgid="2441578748772529288">"Готово"</string>
+    <string name="label_send_key" msgid="2815056534433717444">"Испрати"</string>
+    <string name="label_to_alpha_key" msgid="4793983863798817523">"АБВ"</string>
+    <!-- no translation found for label_to_symbol_key (8516904117128967293) -->
+    <skip />
+    <!-- no translation found for label_to_symbol_with_microphone_key (9035925553010061906) -->
+    <skip />
+    <!-- no translation found for label_pause_key (181098308428035340) -->
+    <skip />
+    <!-- no translation found for label_wait_key (6402152600878093134) -->
+    <skip />
+    <!-- no translation found for spoken_use_headphones (896961781287283493) -->
+    <skip />
+    <!-- no translation found for spoken_current_text_is (2485723011272583845) -->
+    <skip />
+    <!-- no translation found for spoken_no_text_entered (7479685225597344496) -->
+    <skip />
+    <!-- no translation found for spoken_description_unknown (3197434010402179157) -->
+    <skip />
+    <!-- no translation found for spoken_description_shift (244197883292549308) -->
+    <skip />
+    <!-- no translation found for spoken_description_shift_shifted (1681877323344195035) -->
+    <skip />
+    <!-- no translation found for spoken_description_caps_lock (3276478269526304432) -->
+    <skip />
+    <!-- no translation found for spoken_description_delete (8740376944276199801) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_symbol (5486340107500448969) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_alpha (23129338819771807) -->
+    <skip />
+    <!-- no translation found for spoken_description_to_numeric (591752092685161732) -->
+    <skip />
+    <!-- no translation found for spoken_description_settings (4627462689603838099) -->
+    <skip />
+    <!-- no translation found for spoken_description_tab (2667716002663482248) -->
+    <skip />
+    <!-- no translation found for spoken_description_space (2582521050049860859) -->
+    <skip />
+    <!-- no translation found for spoken_description_mic (615536748882611950) -->
+    <skip />
+    <!-- no translation found for spoken_description_smiley (2256309826200113918) -->
+    <skip />
+    <!-- no translation found for spoken_description_return (8178083177238315647) -->
+    <skip />
+    <!-- no translation found for spoken_description_search (1247236163755920808) -->
+    <skip />
+    <!-- no translation found for spoken_description_dot (40711082435231673) -->
+    <skip />
+    <!-- no translation found for spoken_description_language_switch (5507091328222331316) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_next (8636078276664150324) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_previous (800872415009336208) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_on (5700440798609574589) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_locked (593175803181701830) -->
+    <skip />
+    <!-- no translation found for spoken_description_shiftmode_off (657219998449174808) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_symbol (7183343879909747642) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_alpha (3528307674390156956) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_phone (6520207943132026264) -->
+    <skip />
+    <!-- no translation found for spoken_description_mode_phone_shift (5499629753962641227) -->
+    <skip />
+    <!-- no translation found for voice_input (3583258583521397548) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_main_keyboard (3360660341121083174) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_symbols_keyboard (7203213240786084067) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_off (3745699748218082014) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_main_keyboard (6586544292900314339) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_symbols_keyboard (5233725927281932391) -->
+    <skip />
+    <!-- no translation found for voice_input_modes_summary_off (63875609591897607) -->
+    <skip />
+    <!-- no translation found for configure_input_method (373356270290742459) -->
+    <skip />
+    <!-- no translation found for language_selection_title (1651299598555326750) -->
+    <skip />
+    <!-- no translation found for select_language (3693815588777926848) -->
+    <skip />
+    <!-- no translation found for hint_add_to_dictionary (573678656946085380) -->
+    <skip />
+    <!-- no translation found for has_dictionary (6071847973466625007) -->
+    <skip />
+    <!-- no translation found for prefs_enable_log (6620424505072963557) -->
+    <skip />
+    <!-- no translation found for prefs_description_log (5827825607258246003) -->
+    <skip />
+    <!-- no translation found for keyboard_layout (8451164783510487501) -->
+    <skip />
+    <!-- no translation found for subtype_en_GB (88170601942311355) -->
+    <skip />
+    <!-- no translation found for subtype_en_US (6160452336634534239) -->
+    <skip />
+    <!-- no translation found for subtype_with_layout_en_GB (2179097748724725906) -->
+    <skip />
+    <!-- no translation found for subtype_with_layout_en_US (1362581347576714579) -->
+    <skip />
+    <!-- no translation found for subtype_no_language (141420857808801746) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_qwerty (2956121451616633133) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_qwertz (1177848172397202890) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_azerty (8721460968141187394) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_dvorak (3122976737669823935) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_colemak (4205992994906097244) -->
+    <skip />
+    <!-- no translation found for subtype_no_language_pcqwerty (8840928374394180189) -->
+    <skip />
+    <!-- no translation found for custom_input_styles_title (8429952441821251512) -->
+    <skip />
+    <!-- no translation found for add_style (6163126614514489951) -->
+    <skip />
+    <!-- no translation found for add (8299699805688017798) -->
+    <skip />
+    <!-- no translation found for remove (4486081658752944606) -->
+    <skip />
+    <!-- no translation found for save (7646738597196767214) -->
+    <skip />
+    <!-- no translation found for subtype_locale (8576443440738143764) -->
+    <skip />
+    <!-- no translation found for keyboard_layout_set (4309233698194565609) -->
+    <skip />
+    <!-- no translation found for custom_input_style_note_message (8826731320846363423) -->
+    <skip />
+    <!-- no translation found for enable (5031294444630523247) -->
+    <skip />
+    <!-- no translation found for not_now (6172462888202790482) -->
+    <skip />
+    <!-- no translation found for custom_input_style_already_exists (8008728952215449707) -->
+    <skip />
+    <!-- no translation found for prefs_usability_study_mode (1261130555134595254) -->
+    <skip />
+    <!-- no translation found for prefs_keypress_vibration_duration_settings (1829950405285211668) -->
+    <skip />
+    <!-- no translation found for prefs_keypress_sound_volume_settings (5875933757082305040) -->
+    <skip />
+</resources>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-ms/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-ms/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-ms/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-ms/strings-appname.xml b/java/res/values-ms/strings-appname.xml
new file mode 100644
index 0000000..73b5537
--- /dev/null
+++ b/java/res/values-ms/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Papan kekunci Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Penyemak ejaan Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Tetapan papan kekunci Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Tetapan penyemakan ejaan"</string>
+</resources>
diff --git a/java/res/values-ms/strings.xml b/java/res/values-ms/strings.xml
index a07c44f..a7040b7 100644
--- a/java/res/values-ms/strings.xml
+++ b/java/res/values-ms/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Papan kekunci Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Papan kekunci Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Tetapan papan kekunci Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Pilihan input"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Penyemak ejaan Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Arahan Log Penyelidikan"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Penyemak ejaan Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Tetapan penyemakan ejaan"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Cari nama kenalan"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Penyemak ejaan menggunakan entri dari senarai kenalan anda"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Getar pada tekanan kekunci"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Pilihan untuk pakar"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Tukar ke kaedah input lain"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Kunci pertukaran bahasa meliputi kaedah masukan lain juga"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Tekan kunci alih bahasa"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Pop tmbl knci ketpkn lengah"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Tiada lengah"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Lalai"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Cadangkan nama Kenalan"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Menggunakan nama daripada Kenalan untuk cadangan dan pembetulan"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Dayakan pembetulan semula"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Tetapkan cadangan untuk pembetulan semula"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Huruf besar auto"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Kamus tambahan"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Kamus utama"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Sederhana"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresif"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Sangat agresif"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Cadangan perkataan seterusnya"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Gunakan perkataan sebelumnya untuk memperbaik cadangan"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Ramalan perkataan seterusnya"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Gunakan juga perkataan sebelumnya untuk ramalan"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Cadangan perkataan seterusnya"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Berdasarkan perkataan sebelumnya"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Input gerak isyarat"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Masukkan perkataan dengan menyurih huruf perkataan itu."</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Tunjukkan jejak gerak isyarat"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Tunjukkan perkataan gerak isyarat"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Tunjukkan perkataan pratonton terapung dengan gerak isyarat"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Disimpan"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Pergi"</string>
     <string name="label_next_key" msgid="362972844525672568">"Seterusnya"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Carian"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Titik"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Tukar bahasa"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Seterusnya"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Sebelumnya"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Kunci anjak didayakan"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Kunci huruf besar didayakan"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Kunci anjak dilumpuhkan"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-nb/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-nb/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-nb/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-nb/strings-appname.xml b/java/res/values-nb/strings-appname.xml
new file mode 100644
index 0000000..56c1c3c
--- /dev/null
+++ b/java/res/values-nb/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android-tastatur"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android-stavekontroll"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Innstillinger for Android-tastatur"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Innstillinger for stavekontroll"</string>
+</resources>
diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml
index 75c1295..cbde87f 100644
--- a/java/res/values-nb/strings.xml
+++ b/java/res/values-nb/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Skjermtastatur"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-tastatur (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Innstillinger for skjermtastatur"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Inndataalternativer"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-stavekontroll"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Kommandoer for undersøkelseslogging"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-stavekontroll (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Innstillinger for stavekontroll"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Slå opp kontaktnavn"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Stavekontrollen bruker oppføringer fra kontaktlisten din"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrer ved tastetrykk"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Alternativer for eksperter"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Bytt inndatametode"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Tasten for språkbytte dekker også andre inndatametoder"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Skjul språkbyttetasten"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Tregt tastevindu"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"U/ forsinkelse"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Standard"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Foreslå kontaktnavn"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Bruk navn fra Kontakter til forslag og korrigeringer"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Aktiver korrigeringer"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Angi forslag for korrigeringer"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Stor forbokstav"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Tilleggsordbøker"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Hovedordliste"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Moderat"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Omfattende"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Veldig aggressiv"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Forslag til rettelser av neste ord"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Bruk forrige ord til å forbedre forslagene"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Forslag til neste ord"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Bruk forrige ord også for forslag"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Forslag til neste ord"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Basert på det forrige ordet"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Bevegelseskommandoer"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Angi et ord ved å skrive bokstavene på skjermen med fingeren"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Vis bevegelsesspor"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Vis bevegelsesord"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Vis flytende forhåndsvisningsord under bevegelser"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: Lagret"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Utfør"</string>
     <string name="label_next_key" msgid="362972844525672568">"Neste"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Søk"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Prikk"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Bytt språk"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Neste"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Forrige"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift er aktivert"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock er aktivert"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift er deaktivert"</string>
diff --git a/java/res/values-nl/bools.xml b/java/res/values-nl/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-nl/bools.xml
+++ b/java/res/values-nl/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
diff --git a/java/res/values-nl/strings-appname.xml b/java/res/values-nl/strings-appname.xml
new file mode 100644
index 0000000..ee288ef
--- /dev/null
+++ b/java/res/values-nl/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android-toetsenbord"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Spellingcontrole van Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Instellingen voor Android-toetsenbord"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Instellingen voor spellingcontrole"</string>
+</resources>
diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml
index a7d499b..d8a582d 100644
--- a/java/res/values-nl/strings.xml
+++ b/java/res/values-nl/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android-toetsenbord"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-toetsenbord (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Instellingen voor Android-toetsenbord"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Invoeropties"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Spellingcontrole van Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Opdrachten in onderzoekslogbestand"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Spellingcontrole van Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Instellingen voor spellingcontrole"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Contactnamen opzoeken"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"De spellingcontrole gebruikt items uit uw contactenlijst"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Trillen bij toetsaanslag"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opties voor experts"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Invoermeth. overschakelen"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Schakelknop voor taal ook van toepassing op andere invoermethoden"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Taal schakelen onderdr."</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Afwijz.vertr. toetspop-up"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Geen vertraging"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Standaard"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Contactnamen suggereren"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Namen uit Contacten gebruiken voor suggesties en correcties"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Verbeteringen inschakelen"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Suggesties instellen voor verbeteringen"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Auto-hoofdlettergebruik"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Woordenboeken toevoegen"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Algemeen woordenboek"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Normaal"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agressief"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Zeer agressief"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Volgende woordsuggesties"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Vorig woord gebruiken om suggesties te verbeteren"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Volgende woordvoorspelling"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Het voorgaande woord ook voor voorspelling gebruiken"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Suggesties voor volgend woord"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Op basis van het vorige woord"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Invoer met gebaren"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Voer een woord in door de letters van een woord te volgen"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Gebarenspoor weergeven"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Woord met gebaar weergeven"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Zwevend voorbeeldwoord met gebaar weergeven"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: opgeslagen"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Start"</string>
     <string name="label_next_key" msgid="362972844525672568">"Verder"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Zoeken"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Stip"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Taal wijzigen"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Volgende"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Vorige"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift ingeschakeld"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock ingeschakeld"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift uitgeschakeld"</string>
diff --git a/java/res/values-pl/bools.xml b/java/res/values-pl/bools.xml
index 897f4b3..c289e5b 100644
--- a/java/res/values-pl/bools.xml
+++ b/java/res/values-pl/bools.xml
@@ -1,22 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
-	<bool name="im_is_default">true</bool>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
 </resources>
diff --git a/java/res/values-pl/strings-appname.xml b/java/res/values-pl/strings-appname.xml
new file mode 100644
index 0000000..e460644
--- /dev/null
+++ b/java/res/values-pl/strings-appname.xml
@@ -0,0 +1,30 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Klawiatura Android"</string>
+    <!-- no translation found for spell_checker_service_name (6268342166872202903) -->
+    <skip />
+    <!-- no translation found for english_ime_settings (7470027018752707691) -->
+    <skip />
+    <!-- no translation found for android_spell_checker_settings (8397842018475560441) -->
+    <skip />
+</resources>
diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml
index bf27782..e54a787 100644
--- a/java/res/values-pl/strings.xml
+++ b/java/res/values-pl/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Klawiatura Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Klawiatura Androida (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Ustawienia klawiatury Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opcje wprowadzania"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Słownik Androida"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Polecenia dziennika badań"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Sprawdzanie pisowni na Androidzie (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Ustawienia sprawdzania pisowni"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Przeszukaj kontakty"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Sprawdzanie pisowni bierze pod uwagę wpisy z listy kontaktów."</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Wibracja przy naciśnięciu"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opcje dla ekspertów"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Włącz inne metody wprowadzania"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Klawisz zmiany języka obejmuje też inne metody wprowadzania"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Wyłącz klawisz zmiany języka"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Opóźnienie znikania klawiszy"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Bez opóźnienia"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Wartość domyślna"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Proponuj osoby z kontaktów"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"W propozycjach i poprawkach użyj nazwisk z kontaktów"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Włącz poprawki"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Ustaw sugestie poprawek"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Wstawiaj wielkie litery"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Dodatkowe słowniki"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Słownik główny"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Umiarkowana"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresywna"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Bardzo agresywna"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Sugestie kolejnych słów"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Używaj poprzedniego wyrazu, by polepszyć sugestie"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Przewidywanie następnego wyrazu"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Przewiduj również na podstawie poprzedniego słowa"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Sugestie kolejnych słów"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Na podstawie poprzedniego słowa"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Wprowadzanie gestem"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Podaj słowo, pisząc litery palcem"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Pokazuj ślad gestu"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Podpowiadaj przy pisaniu gestem"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Pokazuj pływający podgląd słowa podczas pisania gestem"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Zapisano"</string>
     <string name="label_go_key" msgid="1635148082137219148">"OK"</string>
     <string name="label_next_key" msgid="362972844525672568">"Dalej"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Szukaj"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punkt"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Przełącz język"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Dalej"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Wstecz"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift włączony"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock włączony"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift wyłączony"</string>
diff --git a/java/res/values-pt-rPT/strings-appname.xml b/java/res/values-pt-rPT/strings-appname.xml
new file mode 100644
index 0000000..1b88acb
--- /dev/null
+++ b/java/res/values-pt-rPT/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Teclado do Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Verificador ortográfico do Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Definições de teclado do Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Definições da verificação ortográfica"</string>
+</resources>
diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml
index ccb042e..d761614 100644
--- a/java/res/values-pt-rPT/strings.xml
+++ b/java/res/values-pt-rPT/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Teclado do Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Definições de teclado do Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opções de introdução"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Verificador ortográfico do Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comandos de Reg. Invest."</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Verificador ortográfico do Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Definições da verificação ortográfica"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Procurar nomes de contac."</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"O corretor ortográfico utiliza entradas da sua lista de contactos"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrar ao primir as teclas"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opções para especialistas"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Mudar p/ outros mét. ent."</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"A tecla de mudança de idioma abrange outros métodos de entrada"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Suprimir tecla alt idioma"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Atraso p/ ignorar pop-up"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Sem atraso"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Predefinido"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Sugerir nomes de Contactos"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Utilizar nomes dos Contactos para sugestões e correções"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Ativar correções"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Definir sugestões para correções"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Letras maiúsculas automáticas"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Dicionários extras"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Dicionário principal"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Moderada"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agressiva"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Muito agressivo"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Sugestões da palavra seguinte"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Utilizar palavra anterior para melhorar sugestões"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Previsão da palavra seguinte"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Utilizar a palavra anterior também para predição"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Sugestões da palavra seguinte"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Com base na palavra anterior"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Introd. por gestos"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Introduza uma palavra, desenhando as letras de uma palavra"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Mostrar percurso do gesto"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Mostrar palavra do gesto"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Mostrar visualização flutuante da palavra com gesto"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: guardada"</string>
     <string name="label_go_key" msgid="1635148082137219148">"OK"</string>
     <string name="label_next_key" msgid="362972844525672568">"Avançar"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Pesquisar"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Ponto"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Mudar de idioma"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Seguinte"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Anterior"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift ativado"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock ativado"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift desativado"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-pt/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-pt/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-pt/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-pt/strings-appname.xml b/java/res/values-pt/strings-appname.xml
new file mode 100644
index 0000000..d78786d
--- /dev/null
+++ b/java/res/values-pt/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Teclado do Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Corretor ortográfico do Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Configurações de teclado do Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Configurações de verificação ortográfica"</string>
+</resources>
diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml
index 7d64759..11f8d54 100644
--- a/java/res/values-pt/strings.xml
+++ b/java/res/values-pt/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Teclado Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Teclado Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Configurações de teclado Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opções de entrada"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Corretor ortográfico do Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Pesq. comandos de reg."</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Corretor ortográfico do Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Configurações de verificação ortográfica"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Buscar nomes de contatos"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"O corretor ortográfico usa entradas de sua lista de contatos"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrar ao tocar a tecla"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opções para especialistas"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Outros métodos de entrada"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"A tecla p/ mudar o idioma também cobre outros métodos de entrada"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Ocult. tecla mudar idioma"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Dispens. atraso chave princ."</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Sem atraso"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Padrão"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Sugerir nomes de contato"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Usar nomes dos Contatos para sugestões e correções"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Ativar recorreções"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Definir sugestões para recorreções"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Capitaliz. automática"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Dicionários complementares"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Dicionário principal"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Moderado"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agressivo"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Muito agressivo"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Sugestões p/ palavra seguinte"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Usar palavra anterior para melhorar as sugestões"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Previsão da palavra seguinte"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Use também a palavra anterior para prever"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Sugestões para a palavra seguinte"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Com base na palavra anterior"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Entrada por gesto"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Introduza uma palavra traçando suas letras"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Mostrar percurso do gesto"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Mostrar palavra do gesto"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Mostrar visualização flutuante da palavra com gesto"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Salvo"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Ir"</string>
     <string name="label_next_key" msgid="362972844525672568">"Avançar"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Voltar"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Pesquisar"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Ponto"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Alterar idioma"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Próximo"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Anterior"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift ativado"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock ativado"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift desativado"</string>
diff --git a/java/res/values-rm/strings.xml b/java/res/values-rm/strings.xml
index 18741f8..84400a4 100644
--- a/java/res/values-rm/strings.xml
+++ b/java/res/values-rm/strings.xml
@@ -20,18 +20,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Tastatura Android"</string>
     <!-- no translation found for aosp_android_keyboard_ime_name (7877134937939182296) -->
     <skip />
-    <string name="english_ime_settings" msgid="6661589557206947774">"Parameters da la tastatura Android"</string>
     <!-- no translation found for english_ime_input_options (3909945612939668554) -->
     <skip />
-    <!-- no translation found for spell_checker_service_name (7338064335159755926) -->
+    <!-- no translation found for english_ime_research_log (8492602295696577851) -->
     <skip />
     <!-- no translation found for aosp_spell_checker_service_name (6985142605330377819) -->
     <skip />
-    <!-- no translation found for android_spell_checker_settings (5822324635435443689) -->
-    <skip />
     <!-- no translation found for use_contacts_for_spellchecking_option_title (5374120998125353898) -->
     <skip />
     <!-- no translation found for use_contacts_for_spellchecking_option_summary (8754413382543307713) -->
@@ -53,7 +49,9 @@
     <skip />
     <!-- no translation found for include_other_imes_in_language_switch_list_summary (840637129103317635) -->
     <skip />
-    <!-- no translation found for suppress_language_switch_key (8003788410354806368) -->
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
     <skip />
     <!-- no translation found for key_preview_popup_dismiss_delay (6213164897443068248) -->
     <skip />
@@ -65,10 +63,6 @@
     <skip />
     <!-- no translation found for use_contacts_dict_summary (6599983334507879959) -->
     <skip />
-    <!-- no translation found for enable_span_insert (7204653105667167620) -->
-    <skip />
-    <!-- no translation found for enable_span_insert_summary (2947317657871394467) -->
-    <skip />
     <string name="auto_cap" msgid="1719746674854628252">"Maiusclas automaticas"</string>
     <!-- no translation found for configure_dictionaries_title (4238652338556902049) -->
     <skip />
@@ -96,13 +90,19 @@
     <skip />
     <!-- no translation found for auto_correction_threshold_mode_very_aggeressive (3386782235540547678) -->
     <skip />
-    <!-- no translation found for bigram_suggestion (8169311444438922902) -->
+    <!-- no translation found for bigram_prediction (5809665643352206540) -->
     <skip />
-    <!-- no translation found for bigram_suggestion_summary (6635527607242625713) -->
+    <!-- no translation found for bigram_prediction_summary (3253961591626441019) -->
     <skip />
-    <!-- no translation found for bigram_prediction (3216364899483135294) -->
+    <!-- no translation found for gesture_input (3310827802759290774) -->
     <skip />
-    <!-- no translation found for bigram_prediction_summary (1747261921174300098) -->
+    <!-- no translation found for gesture_input_summary (7019742443455085809) -->
+    <skip />
+    <!-- no translation found for gesture_preview_trail (3802333369335722221) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text (6859416520117939680) -->
+    <skip />
+    <!-- no translation found for gesture_floating_preview_text_summary (3333754126434989709) -->
     <skip />
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Memorisà"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Dai"</string>
@@ -159,6 +159,12 @@
     <skip />
     <!-- no translation found for spoken_description_dot (40711082435231673) -->
     <skip />
+    <!-- no translation found for spoken_description_language_switch (5507091328222331316) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_next (8636078276664150324) -->
+    <skip />
+    <!-- no translation found for spoken_description_action_previous (800872415009336208) -->
+    <skip />
     <!-- no translation found for spoken_description_shiftmode_on (5700440798609574589) -->
     <skip />
     <!-- no translation found for spoken_description_shiftmode_locked (593175803181701830) -->
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-ro/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-ro/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-ro/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-ro/strings-appname.xml b/java/res/values-ro/strings-appname.xml
new file mode 100644
index 0000000..dfa6422
--- /dev/null
+++ b/java/res/values-ro/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Tastatură Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Verificator ortografic Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Setările tastaturii Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Setările de verificare ortografică"</string>
+</resources>
diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml
index 0c4d8bc..82197ba 100644
--- a/java/res/values-ro/strings.xml
+++ b/java/res/values-ro/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Tastatură Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Tastatură Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Setările tastaturii Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Opţiuni de introducere text"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Verificator ortografic Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Comenzi jurnal cercetare"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Verificator ortografic Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Setări de verificare ortografică"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Verificare nume în agendă"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Verificatorul ortografic utilizează intrări din lista de contacte"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrare la apăsarea tastei"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Opţiuni pentru experţi"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Comut. alte metode de introd."</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Tasta de comutare între limbi include şi alte metode de introd."</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Suprim. tasta comut. limbi"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Înt. înch. pop-up esenţ."</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Fără întârziere"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Prestabilit"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Sugeraţi nume din Agendă"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Utilizaţi numele din Agendă pentru sugestii şi corecţii"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Activaţi rectificările"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Setaţi sugestii pentru rectificări"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Auto-capitalizare"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Dicţionare suplimentare"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Dicţionar principal"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Moderată"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresivă"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Foarte exigentă"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Sugestii pentru cuvântul următor"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Utilizaţi cuvântul anterior pentru a îmbunătăţi sugestiile"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Predicţia cuvântului următor"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Se utilizează şi cuvântul precedent pentru predicţii"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Sugestii pentru cuvântul următor"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Bazate pe cuvântul precedent"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Utilizaţi gesturi"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Introduceţi un cuvânt desenând literele acestuia"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Se afişează urma gestului"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Sugestie cuvinte la gesturi"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Se afişează previzualizarea cuvântului flotant la gesturi"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: salvat"</string>
     <string name="label_go_key" msgid="1635148082137219148">"OK"</string>
     <string name="label_next_key" msgid="362972844525672568">"Înainte"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Căutaţi"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punct"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Schimbaţi limba"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Înainte"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Înapoi"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Tasta Shift a fost activată"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Tasta Caps Lock a fost activată"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Tasta Shift a fost dezactivată"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-ru/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-ru/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-ru/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-ru/strings-appname.xml b/java/res/values-ru/strings-appname.xml
new file mode 100644
index 0000000..5db1d0b
--- /dev/null
+++ b/java/res/values-ru/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Клавиатура Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Проверка правописания Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Настройки клавиатуры Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Настройки проверки правописания"</string>
+</resources>
diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml
index f76cc8e..5f1953c 100644
--- a/java/res/values-ru/strings.xml
+++ b/java/res/values-ru/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Клавиатура Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавиатура Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Клавиатура Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Настройки"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Проверка правописания Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Все команды"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Проверка правописания Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Настройка проверки правописания"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Поиск контактов"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Обращаться к списку контактов при проверке правописания"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Виброотклик клавиш"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Для опытных пользователей"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Другой способ ввода"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Клавиша переключения языков также служит для смены способа ввода"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Блок. кл. перекл. языков"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Задержка закрытия"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Без задержки"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"По умолчанию"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Подсказывать имена"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Подсказывать исправления на основе имен из списка контактов"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Автоисправление"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Показывать варианты исправления"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Заглавные автоматически"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Дополнительные словари"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Основной словарь"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Умеренное"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Активное"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Очень активно"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Следующие варианты"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Использовать предыдущее слово, чтобы исправить предложенные варианты"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Следующая подсказка"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Использовать предыдущее слово для прогнозирования"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Подсказка для следующего слова"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Подсказки, основанные на предыдущих словах"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Ввод жестами"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Перемещайте палец от буквы к букве, чтобы составить слово"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Рисовать линию"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Подсказывать слово"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Показывать подсказку во время ввода"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: сохранено"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Поиск"</string>
     <string name="label_next_key" msgid="362972844525672568">"Далее"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Клавиша \"Ввод\""</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Поиск"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Точка"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Сменить язык"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Далее"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Назад"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Верхний регистр включен"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock включен"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Верхний регистр отключен"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-sk/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-sk/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-sk/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-sk/strings-appname.xml b/java/res/values-sk/strings-appname.xml
new file mode 100644
index 0000000..5b55900
--- /dev/null
+++ b/java/res/values-sk/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Klávesnica Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Kontrola pravopisu Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Nastavenia klávesnice Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Nastavenia kontroly pravopisu"</string>
+</resources>
diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml
index 44eecdc..8d540eb 100644
--- a/java/res/values-sk/strings.xml
+++ b/java/res/values-sk/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Klávesnica Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Klávesnica Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Nastavenia klávesnice Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Možnosti zadávania textu a údajov"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Kontrola pravopisu Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Príkazy denníka výskumu"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Kontrola pravopisu Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Nastavenia kontroly pravopisu"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Vyhľadať kontakty"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Kontrola pravopisu používa záznamy z vášho zoznamu kontaktov"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Pri stlačení klávesu vibrovať"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Možnosti pre odborníkov"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Prepnúť na iné metódy vstupu"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Kláves na prepnutie jazyka pokrýva aj ďalšie metódy vstupu"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Blok. kláves prep. jazyka"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Onesk. zrušenia kľúč. kon. okna"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Bez oneskorenia"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Predvolená"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Navrhnúť mená kontaktov"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Používať mená z Kontaktov na návrhy a opravy"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Povoliť opätovné opravy"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Nastaviť návrhy pre opätovné opravy"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Veľké písmená automaticky"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Doplnkové slovníky"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Hlavný slovník"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Mierne"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresívne"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Veľmi agresívne"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Návrhy ďalšieho slova"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Na zlepšenie návrhov použiť predchádzajúce slovo"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Odhad ďalšieho slova"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Použiť predchádzajúce slovo aj pre predpoveď"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Návrhy ďalšieho slova"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Na základe predchádzajúceho slova"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Zadávanie gestami"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Napíšte slovo zadaním jeho písmen"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Zobrazovať stopu gesta"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Zobrazovať slovo gesta"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Zobrazovať plávajúcu ukážku slova gesta"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Uložené"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Hľadať"</string>
     <string name="label_next_key" msgid="362972844525672568">"Ďalej"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Hľadať"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Bodka"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Prepnúť jazyk"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Ďalšie"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Predchádzajúce"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Kláves Shift je povolený"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Kláves Caps Lock je povolený"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Kláves Shift je zakázaný"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-sl/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-sl/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-sl/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-sl/strings-appname.xml b/java/res/values-sl/strings-appname.xml
new file mode 100644
index 0000000..fd303d8
--- /dev/null
+++ b/java/res/values-sl/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Tipkovnica Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Črkovalnik za Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Nastavitve tipkovnice Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Nastavitve preverjanja črkovanja"</string>
+</resources>
diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml
index ce7dc70..9c1a9e6 100644
--- a/java/res/values-sl/strings.xml
+++ b/java/res/values-sl/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Tipkovnica Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Tipkovnica Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Nastavitve tipkovnice Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Možnosti vnosa"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Črkovalnik za Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Ukazi za dnevnik raziskav"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Črkovalnik za Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Nastavitve preverjanja črkovanja"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Iskanje imen stikov"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Črkovalnik uporablja vnose s seznama stikov"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibriranje ob pritisku tipke"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Možnosti za strokovnjake"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Prekl. na drug nač. vnosa"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Tipka za preklop jezika, ki vključuje tudi druge načine vnosa"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Onemogoči tipko za preklop jezika"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Trajanje povečanja tipke"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Brez zakasnitve"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Privzeto"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Predlagaj imena stikov"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Uporaba imen iz stikov za predloge in popravke"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Omogoči vnovične popravke"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Nastavitev predlogov za vnovične popravke"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Samod. velike začetnice"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Dodatni slovarji"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Glavni slovar"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Zmerno"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Strogo"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Zelo strogo"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Predlogi naslednje besede"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Predloge izboljšaj s prejšnjo besedo"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Predvidevanje naslednje besede"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Uporabi prejšnjo besedo tudi za predvidevanje"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Predlogi za naslednjo besedo"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Na podlagi prejšnje besede"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Vnos s potezo"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Vnašanje besede z drsenjem po zaslonu od črke do črke"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Prikaži pot poteze"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Pokaži besedo za potezo"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Pokaži plavajoči predogled besede za potezo"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: shranjeno"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Pojdi"</string>
     <string name="label_next_key" msgid="362972844525672568">"Naprej"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Vračalka"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Iskanje"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Pika"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Preklop jezika"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Naprej"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Nazaj"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Način »Shift« je omogočen"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Način »Caps Lock« je omogočen"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Način »Shift« je onemogočen"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-sr/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-sr/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-sr/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-sr/strings-appname.xml b/java/res/values-sr/strings-appname.xml
new file mode 100644
index 0000000..449fe55
--- /dev/null
+++ b/java/res/values-sr/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android тастатура"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android провера правописа"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Подешавања Android тастатуре"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Подешавања провере правописа"</string>
+</resources>
diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml
index 6d2899b..af20852 100644
--- a/java/res/values-sr/strings.xml
+++ b/java/res/values-sr/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android тастатура"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android тастатура (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Подешавања Android тастатуре"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Опције уноса"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android провера правописа"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Команде евиденције истраживања"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android провера правописа (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Подешавања провере правописа"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Потражи имена контаката"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Контролор правописа користи уносе са листе контаката"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Вибрирај на притисак тастера"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Опције за стручњаке"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Пребаци на друге методе уноса"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Тастер за пребацивање језика обухвата и друге методе уноса"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Искључи тастер за језике"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Одложи одбац. иск. прозора тастера"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Без одлагања"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Подразумевано"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Предложи имена контаката"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Користи имена из Контаката за предлоге и исправке"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Омогући поновне исправке"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Подешавање предлога за поновне исправке"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Аутоматски унос великих слова"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Помоћни речници"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Главни речник"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Умерено"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Агресивно"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Веома агресивно"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Предлози за следећу реч"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Користи претходну реч за побољшање предлога"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Предвиђање следеће речи"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Користи претходну реч и за предвиђање"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Предлози за следећу реч"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"На основу претходне речи"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Унос покретом"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Унесите реч исписивањем слова речи"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Прикажи траг покрета"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Прикажи реч уз покрет"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Приказ плутајућег прегледа речи уз покрет"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Сачувано"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Иди"</string>
     <string name="label_next_key" msgid="362972844525672568">"Следеће"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Претражи"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Тачка"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Пребаци језик"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Следеће"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Претходно"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift је омогућен"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock је омогућен"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift је онемогућен"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-sv/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-sv/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-sv/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-sv/strings-appname.xml b/java/res/values-sv/strings-appname.xml
new file mode 100644
index 0000000..9b4a7db
--- /dev/null
+++ b/java/res/values-sv/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Androids tangentbord"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Stavningskontroll i Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Inställningar för Androids tangentbord"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Inställningar för stavningskontroll"</string>
+</resources>
diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml
index 75e80d4..1ec59d9 100644
--- a/java/res/values-sv/strings.xml
+++ b/java/res/values-sv/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Androids tangentbord"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Androids tangentbord (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Inställningar för Androids tangentbord"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Inmatningsalternativ"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Stavningskontroll i Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Loggkommandon"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Stavningskontroll i Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Inställningar för stavningskontroll"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Sök namn på kontakter"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"I stavningskontrollen används poster från kontaktlistan"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Vibrera vid tangenttryck"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Alternativ för experter"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Byt till annan inmatning"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Språkbytesknappen omfattar även andra inmatningsmetoder"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Stäng av språkbytesknapp"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Ta bort popup-fördröjning"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Fördröj inte"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Standard"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Föreslå kontaktnamn"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Använd namn från Kontakter för förslag och korrigeringar"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Aktivera omkorrigeringar"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Ställ in förslag för omkorrigeringar"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Automatiska versaler"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Tilläggsordlistor"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Huvudordlistan"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Måttlig"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Aggressiv"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Mycket aggressivt"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Föreslå nästa ord"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Förbättra förslagen med föregående ord"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Förutspå nästa ord"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Använd även föregående ord för att ge förslag"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Föreslå nästa ord"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Baserat på föregående ord"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Gestinmatning"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Skriv ett ord för hand genom att rita bokstäverna i ordet"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Visa spår efter rörelse"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Visa svävande ord för rörelse"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Visa förhandsgranskning av svävande ord med rörelse"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>: sparat"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Kör"</string>
     <string name="label_next_key" msgid="362972844525672568">"Nästa"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Retur"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Sök"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Punkt"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Byt språk"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Nästa"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Föregående"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Skift är aktiverat"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock är aktiverat"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Skift är inaktiverat"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-sw/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-sw/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-sw/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-sw/strings-appname.xml b/java/res/values-sw/strings-appname.xml
new file mode 100644
index 0000000..51de0a6
--- /dev/null
+++ b/java/res/values-sw/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Kibodi ya Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Kikagua tahajia cha Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Mipangilio ya kibodi ya Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Mipangilio ya kukagua tahajia"</string>
+</resources>
diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml
index da51cb3..453b280 100644
--- a/java/res/values-sw/strings.xml
+++ b/java/res/values-sw/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Kibodi ya Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Kicharazio cha Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Mipangilio ya kibodi ya Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Chaguo za uingizaji"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Kikagua tahajia cha Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Amri za Kumbukumbu za Utafiti"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Kikagua tahajia cha Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Mipangilio ya kukagua sarufi"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Angalia majina ya wasiliani"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Kikagua tahajia hutumia ingizo kutoka kwa orodha yako ya anwani"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Tetema unabofya kitufe"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Chaguo za wataalamu"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Badilisha hadi kwa mbinu zingine za ingizo"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Ufunguo wa kubadilisha lugha unashughulikia mbinu zingine za ingizo pia"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Zuia ufunguo wa kubadili lugha"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Kuchelewesha kutupa kitufe ibukizi"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Hakuna kuchelewa"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Chaguo-msingi"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Pendekeza majini ya Anwani"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Tumia majina kutoka kwa Anwani kwa mapendekezo na marekebisho"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Wezesha masahihisho mapya"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Weka mapendekezo kwa ajili ya kusahihisha upya"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Uwekaji wa herufi kubwa kiotomatiki"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Nyongeza za kamusi"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Kamusi kuu"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Ya wastani"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Ya hima"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Changamfu zaidi"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Mapendekezo ya neno lifuatalo"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Tumia neno la awali ili kuboresha mapendekezo"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Utabiri wa neno lifuatalo"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Tumia  neno la awali pia kwa udadisi"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Mapendekezo ya neno lifuatalo"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Kulingana na neno la awali"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Ingizo la ishara"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Ingiza neno kwa kufuatilia herufi za neno"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Onyesha njia ya ishara"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Onyesha neno la ishara"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Onyesha neno hakiki linaloelea lililo na ishara"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Imehifadhiwa"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Nenda"</string>
     <string name="label_next_key" msgid="362972844525672568">"Ifuatayo"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Rudi"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Tafuta"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Nukta"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Badili lugha"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Inayofuata"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Iliyotangulia"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift imewezeshwa"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps lock imewezeshwa"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift imelemazwa"</string>
diff --git a/java/res/values-sw600dp-land/dimens.xml b/java/res/values-sw600dp-land/dimens.xml
index a478df8..51c710f 100644
--- a/java/res/values-sw600dp-land/dimens.xml
+++ b/java/res/values-sw600dp-land/dimens.xml
@@ -53,7 +53,18 @@
     <fraction name="spacebar_text_ratio">30.0%</fraction>
     <dimen name="key_uppercase_letter_padding">4dp</dimen>
 
+    <!-- For 5-row keyboard -->
+    <fraction name="key_bottom_gap_5row">3.20%p</fraction>
+    <fraction name="key_letter_ratio_5row">62%</fraction>
+    <fraction name="key_uppercase_letter_ratio_5row">36%</fraction>
+
     <dimen name="suggestions_strip_padding">252.0dp</dimen>
     <integer name="max_more_suggestions_row">5</integer>
     <fraction name="min_more_suggestions_width">50%</fraction>
+
+    <!-- Gesture floating preview text parameters -->
+    <dimen name="gesture_floating_preview_text_size">26dp</dimen>
+    <dimen name="gesture_floating_preview_text_offset">76dp</dimen>
+    <dimen name="gesture_floating_preview_horizontal_padding">26dp</dimen>
+    <dimen name="gesture_floating_preview_vertical_padding">17dp</dimen>
 </resources>
diff --git a/java/res/values-sw600dp/config.xml b/java/res/values-sw600dp/config.xml
index 2f35d9a..e296623 100644
--- a/java/res/values-sw600dp/config.xml
+++ b/java/res/values-sw600dp/config.xml
@@ -19,6 +19,8 @@
 -->
 
 <resources>
+    <!-- Device form factor. This value must be aligned with {@link KeyboardId.DEVICE_FORM_FACTOR_TABLET7} -->
+    <integer name="config_device_form_factor">1</integer>
     <bool name="config_enable_show_voice_key_option">false</bool>
     <bool name="config_enable_show_popup_on_keypress_option">false</bool>
     <bool name="config_enable_bigram_suggestions_option">false</bool>
@@ -32,8 +34,9 @@
     <string name="config_default_keyboard_theme_index" translatable="false">5</string>
     <integer name="config_max_more_keys_column">5</integer>
     <!--
-        Configuration for LatinKeyboardView
+        Configuration for MainKeyboardView
     -->
+    <dimen name="config_key_hysteresis_distance">40.0dp</dimen>
     <bool name="config_sliding_key_input_enabled">false</bool>
     <!-- Showing more keys keyboard, just above the touched point if true, aligned to the key if
          false -->
diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml
index 5596ba4..586fbe6 100644
--- a/java/res/values-sw600dp/dimens.xml
+++ b/java/res/values-sw600dp/dimens.xml
@@ -66,6 +66,11 @@
     <dimen name="key_preview_height">94.5dp</dimen>
     <dimen name="key_preview_offset">16.0dp</dimen>
 
+    <!-- For 5-row keyboard -->
+    <fraction name="key_bottom_gap_5row">3.20%p</fraction>
+    <fraction name="key_letter_ratio_5row">52%</fraction>
+    <fraction name="key_uppercase_letter_ratio_5row">27%</fraction>
+
     <dimen name="key_preview_offset_ics">8.0dp</dimen>
     <!-- popup_key_height x -0.5 -->
     <dimen name="more_keys_keyboard_vertical_correction_ics">-31.5dp</dimen>
@@ -79,4 +84,13 @@
     <dimen name="suggestion_padding">12dp</dimen>
     <dimen name="suggestion_text_size">22dp</dimen>
     <dimen name="more_suggestions_hint_text_size">33dp</dimen>
+
+    <!-- Gesture preview trail parameters -->
+    <dimen name="gesture_preview_trail_width">2.5dp</dimen>
+    <!-- Gesture floating preview text parameters -->
+    <dimen name="gesture_floating_preview_text_size">28dp</dimen>
+    <dimen name="gesture_floating_preview_text_offset">87dp</dimen>
+    <dimen name="gesture_floating_preview_horizontal_padding">28dp</dimen>
+    <dimen name="gesture_floating_preview_vertical_padding">19dp</dimen>
+    <dimen name="gesture_floating_preview_round_radius">3dp</dimen>
 </resources>
diff --git a/java/res/values-sw768dp-land/dimens.xml b/java/res/values-sw768dp-land/dimens.xml
index b95c858..f4a57ff 100644
--- a/java/res/values-sw768dp-land/dimens.xml
+++ b/java/res/values-sw768dp-land/dimens.xml
@@ -55,8 +55,19 @@
     <fraction name="spacebar_text_ratio">24.00%</fraction>
     <dimen name="key_preview_height">107.1dp</dimen>
 
+    <!-- For 5-row keyboard -->
+    <fraction name="key_bottom_gap_5row">2.65%p</fraction>
+    <fraction name="key_letter_ratio_5row">53%</fraction>
+    <fraction name="key_uppercase_letter_ratio_5row">30%</fraction>
+
     <dimen name="key_preview_offset_ics">8.0dp</dimen>
 
     <dimen name="suggestions_strip_padding">252.0dp</dimen>
     <fraction name="min_more_suggestions_width">50%</fraction>
+
+    <!-- Gesture floating preview text parameters -->
+    <dimen name="gesture_floating_preview_text_size">32dp</dimen>
+    <dimen name="gesture_floating_preview_text_offset">100dp</dimen>
+    <dimen name="gesture_floating_preview_horizontal_padding">32dp</dimen>
+    <dimen name="gesture_floating_preview_vertical_padding">21dp</dimen>
 </resources>
diff --git a/java/res/values-sw768dp/config.xml b/java/res/values-sw768dp/config.xml
index 5fcaeeb..346fa99 100644
--- a/java/res/values-sw768dp/config.xml
+++ b/java/res/values-sw768dp/config.xml
@@ -19,6 +19,8 @@
 -->
 
 <resources>
+    <!-- Device form factor. This value must be aligned with {@link KeyboardId.DEVICE_FORM_FACTOR_TABLET10} -->
+    <integer name="config_device_form_factor">2</integer>
     <bool name="config_enable_show_voice_key_option">false</bool>
     <bool name="config_enable_show_popup_on_keypress_option">false</bool>
     <bool name="config_enable_bigram_suggestions_option">false</bool>
@@ -30,7 +32,7 @@
     <string name="config_default_keyboard_theme_index" translatable="false">5</string>
     <integer name="config_max_more_keys_column">5</integer>
     <!--
-        Configuration for LatinKeyboardView
+        Configuration for MainKeyboardView
     -->
     <bool name="config_sliding_key_input_enabled">false</bool>
     <!-- Showing more keys keyboard, just above the touched point if true, aligned to the key if
diff --git a/java/res/values-sw768dp/dimens.xml b/java/res/values-sw768dp/dimens.xml
index ce33b73..2fd7322 100644
--- a/java/res/values-sw768dp/dimens.xml
+++ b/java/res/values-sw768dp/dimens.xml
@@ -67,6 +67,11 @@
     <dimen name="key_preview_height">94.5dp</dimen>
     <dimen name="key_preview_offset">16.0dp</dimen>
 
+    <!-- For 5-row keyboard -->
+    <fraction name="key_bottom_gap_5row">2.95%p</fraction>
+    <fraction name="key_letter_ratio_5row">51%</fraction>
+    <fraction name="key_uppercase_letter_ratio_5row">33%</fraction>
+
     <dimen name="key_preview_offset_ics">8.0dp</dimen>
     <!-- popup_key_height x -0.5 -->
     <dimen name="more_keys_keyboard_vertical_correction_ics">-31.5dp</dimen>
@@ -80,4 +85,13 @@
     <dimen name="suggestion_padding">8dp</dimen>
     <dimen name="suggestion_text_size">22dp</dimen>
     <dimen name="more_suggestions_hint_text_size">33dp</dimen>
+
+    <!-- Gesture preview trail parameters -->
+    <dimen name="gesture_preview_trail_width">2.5dp</dimen>
+    <!-- Gesture floating preview text parameters -->
+    <dimen name="gesture_floating_preview_text_size">26dp</dimen>
+    <dimen name="gesture_floating_preview_text_offset">86dp</dimen>
+    <dimen name="gesture_floating_preview_horizontal_padding">26dp</dimen>
+    <dimen name="gesture_floating_preview_vertical_padding">17dp</dimen>
+    <dimen name="gesture_floating_preview_round_radius">3dp</dimen>
 </resources>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-th/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-th/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-th/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-th/strings-appname.xml b/java/res/values-th/strings-appname.xml
new file mode 100644
index 0000000..7fc7e3e
--- /dev/null
+++ b/java/res/values-th/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"แป้นพิมพ์แอนดรอยด์"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"การตรวจสอบการสะกดของแอนดรอยด์"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"การตั้งค่าแป้นพิมพ์แอนดรอยด์"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"การตั้งค่าการตรวจสอบการสะกด"</string>
+</resources>
diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml
index da20d66..49124e2 100644
--- a/java/res/values-th/strings.xml
+++ b/java/res/values-th/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"แป้นพิมพ์ Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android keyboard (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"การตั้งค่าแป้นพิมพ์ Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"ตัวเลือกการป้อนข้อมูล"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"แอนดรอยด์ตรวจสอบการสะกด"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"คำสั่งบันทึกการวิจัย"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"แอนดรอยด์ตรวจสอบการสะกด (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"การตั้งค่าการตรวจสอบการสะกด"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"ค้นหารายชื่อติดต่อ"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"เครื่องมือตรวจการสะกดใช้รายการจากรายชื่อติดต่อของคุณ"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"สั่นเมื่อกดปุ่ม"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"ตัวเลือกสำหรับผู้เชี่ยวชาญ"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"ใช้วิธีการป้อนข้อมูลอื่น"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"แป้นสลับภาษาครอบคลุมวิธีการป้อนข้อมูลอื่นๆ ด้วย"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"ยกเลิกแป้นสลับภาษา"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"หน่วงเวลาก่อนปิดป๊อปอัพหลัก"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"ไม่มีการหน่วงเวลา"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"ค่าเริ่มต้น"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"แนะนำชื่อผู้ติดต่อ"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"ใช้ชื่อจากรายชื่อติดต่อสำหรับคำแนะนำและการแก้ไข"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"เปิดใช้งานการแก้ไขซ้ำ"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"ตั้งค่าคำแนะนำสำหรับการแก้ไขซ้ำ"</string>
     <string name="auto_cap" msgid="1719746674854628252">"ปรับเป็นตัวพิมพ์ใหญ่อัตโนมัติ"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"พจนานุกรม Add-On"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"พจนานุกรมหลัก"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"ปานกลาง"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"เข้มงวด"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"เข้มงวดมาก"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"คำแนะนำสำหรับคำถัดไป"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"ใช้คำก่อนหน้านี้เพื่อปรับปรุงคำแนะนำ"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"การคาดคะเนคำถัดไป"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"ใช้คำก่อนหน้านี้สำหรับการคาดคะเน"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"คำแนะนำสำหรับคำถัดไป"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"ตามคำก่อนหน้า"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"ป้อนท่าทางสัมผัส"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"ป้อนคำโดยลากนิ้วเป็นรูปตัวอักษรที่อยู่ในคำ โดยไม่ยกนิ้วขึ้น"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"แสดงรอยทางเดินของท่าทางสัมผัส"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"แสดงคำแนะนำท่าทางสัมผัส"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"แสดงตัวอย่างคำแนะนำพร้อมด้วยท่าทางสัมผัส"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : บันทึกแล้ว"</string>
     <string name="label_go_key" msgid="1635148082137219148">"ไป"</string>
     <string name="label_next_key" msgid="362972844525672568">"ถัดไป"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"ค้นหา"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"เครื่องหมายจุด"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"เปลี่ยนภาษา"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"ถัดไป"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"ก่อนหน้า"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"เปิดใช้งาน Shift แล้ว"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"เปิดใช้งาน Caps Lock แล้ว"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"ปิดใช้งาน Shift แล้ว"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-tl/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-tl/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-tl/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-tl/strings-appname.xml b/java/res/values-tl/strings-appname.xml
new file mode 100644
index 0000000..fd2b3f5
--- /dev/null
+++ b/java/res/values-tl/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Keyboard ng Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Spell checker ng Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Mga setting ng keyboard ng Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Mga setting ng pag-spell check"</string>
+</resources>
diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml
index 7d365b2..dae846c 100644
--- a/java/res/values-tl/strings.xml
+++ b/java/res/values-tl/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android keyboard"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android keyboard (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Mga setting ng Android keyboard"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Mga pagpipilian sa input"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Pang-check ng pagbabaybay ng Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Cmmnd sa Log ng Pnnliksik"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Pang-check ng pagbabaybay ng Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Mga setting ng pang-check ng pagbabaybay"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Maghanap pangalan contact"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Gumagamit pang-check pagbabaybay entry sa iyong listahan contact"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Mag-vibrate sa keypress"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Mga pagpipilian para sa mga dalubhasa"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Lipat iba paraan ng input"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Saklaw din ng key ng pagpalit ng wika ang ibang paraan ng input"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Pigilan key pagpalit wika"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Balewala antala key popup"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Walang antala"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Default"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Mungkahi pangalan Contact"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Gamitin pangalan mula Mga Contact sa mga mungkahi\'t pagwawasto"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Paganahin ang mga muling pagtatama"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Magtakda ng mga suhestyon para sa mga muling pagtatama"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Auto-capitalization"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Mga diksyunaryo na add-on"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Pangunahing diksyunaryo"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Modest"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresibo"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Napaka-agresibo"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Mga paghuhula sa susunod na salita"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Gamitin ang naunang salita para mapahusay ang mga suhestiyon"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Paghuhula sa susunod na salita"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Gamitin ang nakaraang salita para din sa hula"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Mga suhestiyon sa susunod na salita"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Batay sa nakaraang salita"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Pagpasok ng galaw"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Magpasok ng salita sa gamit ang pagsulat sa mga titik ng salita"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Ipakita ang trail ng galaw"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Ipakita ang salita ng galaw"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Ipakita ang lumulutang na preview ng salita na may galaw"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Na-save"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Punta"</string>
     <string name="label_next_key" msgid="362972844525672568">"Susunod"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Bumalik"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Paghahanap"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Tuldok"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Magpalit ng wika"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Susunod"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Nakaraan"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Pinagana ang shift"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Pinagana ang caps lock"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Hindi pinagana ang shift"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-tr/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-tr/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-tr/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-tr/strings-appname.xml b/java/res/values-tr/strings-appname.xml
new file mode 100644
index 0000000..f5e36d2
--- /dev/null
+++ b/java/res/values-tr/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android klavyesi"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android yazım denetleyici"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android klavye ayarları"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Yazım denetimi ayarları"</string>
+</resources>
diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml
index b2f7391..82ca4b3 100644
--- a/java/res/values-tr/strings.xml
+++ b/java/res/values-tr/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android klavyesi"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android klavye (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android klavye ayarları"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Giriş seçenekleri"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android yazım denetleyici"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Araştırma Günlüğü Komutları"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android yazım denetleyici (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Yazım denetimi ayarları"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Kişi adlarını denetle"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Yazım denetleyici, kişi listenizdeki girişleri kullanır"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Tuşa basıldığında titret"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Uzmanlar için seçenekler"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Diğer giriş yöntemine geç"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Dil geçiş tuşu diğer giriş yöntemlerini de kapsar"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Dil geçiş tuşunu gösterme"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Tuş popup\'ının kapatılmasını geciktirme"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Gecikme yok"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Varsayılan"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Kişi Adları öner"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Öneri ve düzeltmeler için Kişiler\'deki adları kullan"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Düzeltmeleri etkinleştir"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Yeniden düzeltmeler için önerileri ayarla"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Otomatik olarak büyük fark yap"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Ek sözlükler"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Ana sözlük"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Ölçülü"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresif"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Çok geniş ölçekte"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Sonraki kelime önerileri"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Önerileri geliştirmek için önceki kelimeyi kullan"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Sonraki kelime tahmini"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Önceki kelimeyi de tahmin için kullan"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Sonraki kelime önerileri"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Önceki kelimeye dayanarak"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Hareket girişi"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Bir kelimeyi harflerini izleyerek girin"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Hareket izini göster"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Hareketle yazılan kelimeyi göster"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Hareketle kayan önizleme kelimesini göster"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Kaydedildi"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Git"</string>
     <string name="label_next_key" msgid="362972844525672568">"İleri"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Enter"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Ara"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Nokta"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Dili değiştir"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Sonraki"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Önceki"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Üst karakter etkin"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Büyük harf kilidi etkin"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Üst karakter devre dışı"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-uk/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-uk/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-uk/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-uk/strings-appname.xml b/java/res/values-uk/strings-appname.xml
new file mode 100644
index 0000000..fdbb89f
--- /dev/null
+++ b/java/res/values-uk/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Клавіатура Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Засіб перевірки орфографії Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Налаштування клавіатури Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Налаштування перевірки орфографії"</string>
+</resources>
diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml
index e994e1a..a3b1b3f 100644
--- a/java/res/values-uk/strings.xml
+++ b/java/res/values-uk/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Клавіатура Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Клавіатура Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Налашт-ня клавіат. Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Парам. введення"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Засіб перевірки орфографії Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Команди журналу дослідж."</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Засіб перевірки орфографії Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Налаштування перевірки орфографії"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Шукати імена контактів"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Програма перевірки правопису використ. записи зі списку контактів"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Вібр. при натисканні клавіш"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Налаштування для досвідчених користувачів"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Інші методи введення"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Клавіша зміни мови дозволяє змінювати методи введення"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Заблок.клавішу зміни мови"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Затримка клавіши закриття"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Без затримки"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"За умовчанням"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Пропон. імена контактів"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Використ. імена зі списку контактів для пропозицій і виправлень"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Увімкнути виправлення"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Показувати варіанти автовиправлень"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Авто викор. вел. літер"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Додані словники"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Основний словник"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Помірне"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Активне"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Дуже активне"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Пропозиції наступного слова"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Використати попереднє слово для покращення пропозицій"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Передбачення наступного слова"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Використовувати попереднє слово також як передбачений запит"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Пропозиції наступного слова"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"На основі попереднього слова"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Введення жестами"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Введіть слово, малюючи літери, з яких воно складається"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Показувати слід жестів"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Показувати слово для жесту"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Показувати спливаючий перегляд слова під час жесту"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : збережено"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Іти"</string>
     <string name="label_next_key" msgid="362972844525672568">"Далі"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Клавіша Return"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Пошук"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Крапка"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Змінити мову"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Далі"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Назад"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift увімкнено"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Caps Lock увімкнено"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift вимкнено"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-vi/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-vi/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-vi/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-vi/strings-appname.xml b/java/res/values-vi/strings-appname.xml
new file mode 100644
index 0000000..6e32d03
--- /dev/null
+++ b/java/res/values-vi/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Bàn phím Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Trình kiểm tra chính tả Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Cài đặt bàn phím Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Cài đặt kiểm tra chính tả"</string>
+</resources>
diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml
index 753af18..6b32c78 100644
--- a/java/res/values-vi/strings.xml
+++ b/java/res/values-vi/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Bàn phím Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Bàn phím Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Cài đặt bàn phím Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Tùy chọn nhập"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Trình kiểm tra chính tả Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Lệnh ghi nhật ký cho nghiên cứu"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Trình kiểm tra chính tả Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Cài đặt kiểm tra chính tả"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Tra cứu tên liên hệ"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Trình kiểm tra chính tả sử dụng các mục nhập từ danh sách liên hệ của bạn"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Rung khi nhấn phím"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Tùy chọn dành cho chuyên gia"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Phương thức nhập khác"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Khóa chuyển ngôn ngữ bao gồm cả các phương thức nhập liệu khác"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Bỏ khóa chuyển ngôn ngữ"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Loại bỏ hiển thị phím trễ"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Không có tgian trễ"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Mặc định"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Đề xuất tên liên hệ"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Sử dụng tên từ Danh bạ cho các đề xuất và chỉnh sửa"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Bật sửa đổi lại"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Đặt đề xuất cho các sửa đổi lại"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Tự động viết hoa"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Thêm từ điển"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Từ điển chính"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Đơn giản"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Linh hoạt"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Rất linh hoạt"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Đề xuất từ tiếp theo"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Sử dụng từ trước đó để cải tiến đề xuất"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Dự đoán từ tiếp theo"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Cũng sử dụng từ trước đó để dự đoán"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Đề xuất từ tiếp theo"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Dựa trên từ trước đó"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Nhập bằng cử chỉ"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Nhập từ bằng cách lần theo các chữ cái của từ"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Hiển thị vệt cử chỉ"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Hiển thị từ theo cử chỉ"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Hiển thị từ xem trước nổi bằng cử chỉ"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Đã lưu"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Tìm"</string>
     <string name="label_next_key" msgid="362972844525672568">"Tiếp theo"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Quay lại"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Tìm kiếm"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Dấu chấm"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Chuyển ngôn ngữ"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Tiếp theo"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Trước"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Đã bật Shift"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Đã bật Caps lock"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Đã tắt Shift"</string>
diff --git a/java/res/values-zh-rCN/strings-appname.xml b/java/res/values-zh-rCN/strings-appname.xml
new file mode 100644
index 0000000..f5e12fd
--- /dev/null
+++ b/java/res/values-zh-rCN/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android 键盘"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android 拼写检查工具"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android 键盘设置"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"拼写检查设置"</string>
+</resources>
diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml
index efde541..f111e4f 100644
--- a/java/res/values-zh-rCN/strings.xml
+++ b/java/res/values-zh-rCN/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android 键盘"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android 键盘 (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android 键盘设置"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"输入选项"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android 拼写检查工具"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"研究记录命令"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android 拼写检查工具 (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"拼写检查设置"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"查找联系人姓名"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"拼写检查工具会使用您的联系人列表中的条目"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"按键振动"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"高级选项"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"切换到其他输入法"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"语言切换键也可用于切换其他输入法"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"隐藏语言切换键"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"弹出字符隐藏延迟"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"无延迟"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"默认"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"联系人姓名建议"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"使用联系人中的姓名提供建议和更正"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"允许再次更正"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"设置建议以用于再次更正"</string>
     <string name="auto_cap" msgid="1719746674854628252">"自动大写"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"附加词典"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"主词典"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"小改"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"大改"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"改动极大"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"下一字词建议"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"使用上一字词改进建议"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"下一字词预测"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"结合前一个字词进行预测"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"下一字词建议"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"根据上一个字词提供建议"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"手势输入"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"连笔书写输入字词"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"显示手势轨迹"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"显示手势文字"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"手势划动输入时显示漂浮预览文字"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>:已保存"</string>
     <string name="label_go_key" msgid="1635148082137219148">"开始"</string>
     <string name="label_next_key" msgid="362972844525672568">"下一步"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"返回"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"搜索"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"点"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"切换语言"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"下一个"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"上一个"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift 模式已启用"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"大写锁定已启用"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift 模式已停用"</string>
diff --git a/java/res/values-zh-rTW/strings-appname.xml b/java/res/values-zh-rTW/strings-appname.xml
new file mode 100644
index 0000000..8cc6638
--- /dev/null
+++ b/java/res/values-zh-rTW/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Android 鍵盤"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Android 拼字檢查"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Android 鍵盤設定"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"拼字檢查設定"</string>
+</resources>
diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml
index 51df022..20edc02 100644
--- a/java/res/values-zh-rTW/strings.xml
+++ b/java/res/values-zh-rTW/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Android 鍵盤"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android 鍵盤 (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Android 鍵盤設定"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"輸入選項"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Android 拼字檢查"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"研究紀錄指令"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android 拼字檢查 (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"拼字檢查設定"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"查詢聯絡人姓名"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"拼字檢查程式使用您的聯絡人清單項目"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"按鍵時震動"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"進階選項"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"切換至其他輸入法"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"使語言切換鍵包含其他輸入法"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"隱藏語言切換鍵"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"關閉彈出式鍵盤的延遲時間"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"不延遲"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"預設"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"建議聯絡人名稱"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"根據「聯絡人」名稱提供建議與修正"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"啟用重新更正"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"設定建議供重新更正"</string>
     <string name="auto_cap" msgid="1719746674854628252">"自動大寫"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"外掛字典"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"主要字典"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"更正範圍小"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"更正範圍大"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"更正範圍極大"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"下一個字詞建議"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"根據前一個字詞找出更適合的建議"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"下一個字詞預測"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"同樣使用前一個字詞進行預測"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"下一個字詞建議"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"根據上一個字詞產生"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"手勢輸入"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"以手指寫出字詞中字母的方式來輸入字詞"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"顯示手勢軌跡"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"顯示手勢字詞"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"使用手勢輸入時顯示浮動預覽字詞"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g>:已儲存"</string>
     <string name="label_go_key" msgid="1635148082137219148">"開始"</string>
     <string name="label_next_key" msgid="362972844525672568">"繼續"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"返回"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"搜尋"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"點"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"切換語言"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"下一步"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"上一步"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"Shift 鍵已啟用"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"大寫鎖定已啟用"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"Shift 鍵已停用"</string>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values-zu/bools.xml
similarity index 76%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values-zu/bools.xml
index c0c45dd..840d20c 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values-zu/bools.xml
@@ -17,10 +17,8 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <!-- Whether this input method should be used as the default for a locale. Override it
+         for supported languages. -->
+    <bool name="im_is_default">true</bool>
+</resources>
diff --git a/java/res/values-zu/strings-appname.xml b/java/res/values-zu/strings-appname.xml
new file mode 100644
index 0000000..a0fb517
--- /dev/null
+++ b/java/res/values-zu/strings-appname.xml
@@ -0,0 +1,27 @@
+<?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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="english_ime_name" msgid="178705338187710493">"Ikhibhodi ye-Android"</string>
+    <string name="spell_checker_service_name" msgid="6268342166872202903">"Isihloli sokupela se-Android"</string>
+    <string name="english_ime_settings" msgid="7470027018752707691">"Izilungiselelo zekhibhodi ye-Android"</string>
+    <string name="android_spell_checker_settings" msgid="8397842018475560441">"Izilungiselelo zokuhlola ukupela"</string>
+</resources>
diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml
index d3f80e4..59c31de 100644
--- a/java/res/values-zu/strings.xml
+++ b/java/res/values-zu/strings.xml
@@ -20,13 +20,10 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="english_ime_name" msgid="7252517407088836577">"Ikhibhodi ye-Android"</string>
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Ikhibhodi ye-Android (AOSP)"</string>
-    <string name="english_ime_settings" msgid="6661589557206947774">"Izilungiselelo zekhibhodi ye-Android"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Okukhethwa kukho kokungenayo"</string>
-    <string name="spell_checker_service_name" msgid="7338064335159755926">"Isihloli sokupela se-Android"</string>
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Imiyalo yefayela lokungena lokucwaninga"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Isihloli sokupela se-Android (AOSP)"</string>
-    <string name="android_spell_checker_settings" msgid="5822324635435443689">"Izilungiselelo zokuhlola ukupela"</string>
     <string name="use_contacts_for_spellchecking_option_title" msgid="5374120998125353898">"Bheka amagama woxhumana nabo"</string>
     <string name="use_contacts_for_spellchecking_option_summary" msgid="8754413382543307713">"Isihloli sokupela sisebenzisa okungenayo kusuka kuhlu lalabo oxhumana nabo"</string>
     <string name="vibrate_on_keypress" msgid="5258079494276955460">"Dlidlizelisa ngokucindezela inkinobho"</string>
@@ -39,14 +36,15 @@
     <string name="advanced_settings_summary" msgid="4487980456152830271">"Izinketho zezingcwenti"</string>
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Shintshela kwezinye izindlela zokungena"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Ukhiye wokushintsha ulimi ubandakanya ezinye izindlela zokungenayo"</string>
-    <string name="suppress_language_switch_key" msgid="8003788410354806368">"Ukhiye wokushintshela wokuvimbela ulimi"</string>
+    <!-- no translation found for show_language_switch_key (5915478828318774384) -->
+    <skip />
+    <!-- no translation found for show_language_switch_key_summary (7343403647474265713) -->
+    <skip />
     <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Ukuvela kokhiye cashisa ukulibazisa"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Cha ukulibazisa"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Okuzenzakalelayo"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Sikisela amagama Othintana nabo"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Amagama abasebenzisi kusuka Kothintana nabo bokusikisela nokulungisa"</string>
-    <string name="enable_span_insert" msgid="7204653105667167620">"Vumela ukulungiswa kabusha"</string>
-    <string name="enable_span_insert_summary" msgid="2947317657871394467">"Setha iziphakamiso zokulungisa kabusha"</string>
     <string name="auto_cap" msgid="1719746674854628252">"Ukwenza ofeleba okuzenzakalelayo"</string>
     <string name="configure_dictionaries_title" msgid="4238652338556902049">"Faka izichazamazwi"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Isichazamazwi sakho ngqangi"</string>
@@ -61,10 +59,13 @@
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Thobekile"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Bukhali"</string>
     <string name="auto_correction_threshold_mode_very_aggeressive" msgid="3386782235540547678">"Nobudlova kakhulu"</string>
-    <string name="bigram_suggestion" msgid="8169311444438922902">"Iziphakamiso zegama elilandelayo"</string>
-    <string name="bigram_suggestion_summary" msgid="6635527607242625713">"Sebenzisa igama elandulele ukuthuthukisa iziphakamiso"</string>
-    <string name="bigram_prediction" msgid="3216364899483135294">"Ukuqagela kwegama elilandelayo"</string>
-    <string name="bigram_prediction_summary" msgid="1747261921174300098">"Sebenzisa igama langaphambilini ukuze uqagele"</string>
+    <string name="bigram_prediction" msgid="5809665643352206540">"Iziphakamiso zegama elilandelayo"</string>
+    <string name="bigram_prediction_summary" msgid="3253961591626441019">"Ngokususela egameni langaphambilini"</string>
+    <string name="gesture_input" msgid="3310827802759290774">"Okokufaka kokuthinta"</string>
+    <string name="gesture_input_summary" msgid="7019742443455085809">"Faka igama ngokulandela ngomkhondo izinhlamvu zegama"</string>
+    <string name="gesture_preview_trail" msgid="3802333369335722221">"Bonisa i-trail yokuthinta"</string>
+    <string name="gesture_floating_preview_text" msgid="6859416520117939680">"Bonisa igama lokuthinta"</string>
+    <string name="gesture_floating_preview_text_summary" msgid="3333754126434989709">"Bonisa igama lokuhlola kuqala elintantayo nokuthinta"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : Kulondoloziwe"</string>
     <string name="label_go_key" msgid="1635148082137219148">"Iya"</string>
     <string name="label_next_key" msgid="362972844525672568">"Okulandelayo"</string>
@@ -95,6 +96,9 @@
     <string name="spoken_description_return" msgid="8178083177238315647">"Buyisela"</string>
     <string name="spoken_description_search" msgid="1247236163755920808">"Sesha"</string>
     <string name="spoken_description_dot" msgid="40711082435231673">"Icashazi"</string>
+    <string name="spoken_description_language_switch" msgid="5507091328222331316">"Shintsha ulimi"</string>
+    <string name="spoken_description_action_next" msgid="8636078276664150324">"Okulandelayo"</string>
+    <string name="spoken_description_action_previous" msgid="800872415009336208">"Okwangaphambilini"</string>
     <string name="spoken_description_shiftmode_on" msgid="5700440798609574589">"U-Shift uvunyelwe"</string>
     <string name="spoken_description_shiftmode_locked" msgid="593175803181701830">"Ofeleba bavunyelwe"</string>
     <string name="spoken_description_shiftmode_off" msgid="657219998449174808">"U-Shift uvimbelwe"</string>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index a18371f..7e8c77e 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -18,13 +18,10 @@
     <declare-styleable name="KeyboardTheme">
         <!-- Keyboard style -->
         <attr name="keyboardStyle" format="reference" />
-        <!-- TODO: Get rid of latinKeyboardStyle -->
-        <!-- LatinKeyboard style -->
-        <attr name="latinKeyboardStyle" format="reference" />
         <!-- KeyboardView style -->
         <attr name="keyboardViewStyle" format="reference" />
-        <!-- LatinKeyboardView style -->
-        <attr name="latinKeyboardViewStyle" format="reference" />
+        <!-- MainKeyboardView style -->
+        <attr name="mainKeyboardViewStyle" format="reference" />
         <!-- MoreKeysKeyboard style -->
         <attr name="moreKeysKeyboardStyle" format="reference" />
         <!-- MoreKeysKeyboardView style -->
@@ -32,7 +29,7 @@
         <attr name="moreKeysKeyboardPanelStyle" format="reference" />
         <!-- Suggestions strip style -->
         <attr name="suggestionsStripBackgroundStyle" format="reference" />
-        <attr name="suggestionsViewStyle" format="reference" />
+        <attr name="suggestionStripViewStyle" format="reference" />
         <attr name="moreSuggestionsViewStyle" format="reference" />
         <attr name="suggestionBackgroundStyle" format="reference" />
         <attr name="suggestionPreviewBackgroundStyle" format="reference" />
@@ -44,26 +41,6 @@
              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" />
-        <!-- 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" />
-        <!-- Large size of the text for keys with multiple letters, in the proportion of key height. -->
-        <attr name="keyLargeLabelRatio" format="float" />
-        <!-- Size of the text for hint letter (= one character hint label), in the proportion of
-             key height. -->
-        <attr name="keyHintLetterRatio" format="float" />
-        <!-- Size of the text for hint label, in the proportion of key height. -->
-        <attr name="keyHintLabelRatio" format="float" />
-        <!-- Size of the text for shifted letter hint, in the proportion of key height. -->
-        <attr name="keyShiftedLetterHintRatio" format="float" />
         <!-- 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.-->
@@ -72,35 +49,19 @@
         <attr name="keyPopupHintLetterPadding" format="dimension" />
         <!-- Right padding of shifted letter hint to the edge of the key.-->
         <attr name="keyShiftedLetterHintPadding" format="dimension" />
-
-        <!-- Color to use for the label in a key. -->
-        <attr name="keyTextColor" format="color" />
-        <!-- Color to use for the label in a key when in inactivated state. -->
-        <attr name="keyTextInactivatedColor" format="color" />
-        <!-- Key hint letter (= one character hint label) color -->
-        <attr name="keyHintLetterColor" format="color" />
-        <!-- Key hint label color -->
-        <attr name="keyHintLabelColor" format="color" />
-        <!-- Shifted letter hint colors -->
-        <attr name="keyShiftedLetterHintInactivatedColor" format="color" />
-        <attr name="keyShiftedLetterHintActivatedColor" format="color" />
+        <!-- Blur radius of key text shadow. -->
+        <attr name="keyTextShadowRadius" format="float" />
 
         <!-- Layout resource for key press feedback.-->
         <attr name="keyPreviewLayout" format="reference" />
-        <!-- The background for key press feedback. -->
-        <attr name="keyPreviewBackground" format="reference" />
-        <!-- The background for the left edge key press feedback. -->
-        <attr name="keyPreviewLeftBackground" format="reference" />
-        <!-- The background for the right edge key press feedback. -->
-        <attr name="keyPreviewRightBackground" format="reference" />
-        <!-- The text color for key press feedback. -->
-        <attr name="keyPreviewTextColor" format="color" />
+        <!-- Key preview background states -->
+        <attr name="state_left_edge" format="boolean" />
+        <attr name="state_right_edge" format="boolean" />
+        <attr name="state_has_morekeys" format="boolean" />
         <!-- Vertical offset of the key press feedback from the key. -->
         <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" />
         <!-- Delay after key releasing and key press feedback dismissing in millisecond -->
         <attr name="keyPreviewLingerTimeout" format="integer" />
 
@@ -110,20 +71,30 @@
         <!-- Layout resource for more keys panel -->
         <attr name="moreKeysLayout" format="reference" />
 
-        <attr name="shadowColor" format="color" />
-        <attr name="shadowRadius" format="float" />
         <attr name="backgroundDimAlpha" format="integer" />
 
-        <attr name="keyTextStyle" format="enum">
-            <!-- This should be aligned with Typeface.NORMAL etc. -->
-            <enum name="normal" value="0" />
-            <enum name="bold" value="1" />
-            <enum name="italic" value="2" />
-            <enum name="boldItalic" value="3" />
-        </attr>
+        <!-- Attributes for PreviewPlacerView -->
+        <attr name="gestureFloatingPreviewTextSize" format="dimension" />
+        <attr name="gestureFloatingPreviewTextColor" format="color" />
+        <attr name="gestureFloatingPreviewTextOffset" format="dimension" />
+        <attr name="gestureFloatingPreviewColor" format="color" />
+        <attr name="gestureFloatingPreviewHorizontalPadding" format="dimension" />
+        <attr name="gestureFloatingPreviewVerticalPadding" format="dimension" />
+        <attr name="gestureFloatingPreviewRoundRadius" 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="gesturePreviewTrailStartWidth" format="dimension" />
+        <attr name="gesturePreviewTrailEndWidth" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="LatinKeyboardView">
+    <declare-styleable name="MainKeyboardView">
         <attr name="autoCorrectionSpacebarLedEnabled" format="boolean" />
         <attr name="autoCorrectionSpacebarLedIcon" format="reference" />
         <!-- Size of the text for spacebar language label, in the proportion of key height. -->
@@ -158,9 +129,9 @@
         <attr name="showMoreKeysKeyboardAtTouchedPoint" format="boolean" />
     </declare-styleable>
 
-    <declare-styleable name="SuggestionsView">
+    <declare-styleable name="SuggestionStripView">
         <attr name="suggestionStripOption" format="integer">
-            <!-- This should be aligned with SuggestionsViewParams.AUTO_CORRECT_* and etc. -->
+            <!-- This should be aligned with SuggestionStripViewParams.AUTO_CORRECT_* and etc. -->
             <flag name="autoCorrectBold" value="0x01" />
             <flag name="autoCorrectUnderline" value="0x02" />
             <flag name="validTypedWordBold" value="0x04" />
@@ -169,13 +140,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>
@@ -319,6 +290,50 @@
         <!-- The X-coordinate of upper right corner of this key including horizontal gap.
              If the value is negative, the origin is the right edge of the keyboard. -->
         <attr name="keyXPos" format="dimension|fraction" />
+
+        <!-- Key top visual attributes -->
+        <attr name="keyTypeface" format="enum">
+            <!-- This should be aligned with Typeface.NORMAL etc. -->
+            <enum name="normal" value="0" />
+            <enum name="bold" value="1" />
+            <enum name="italic" value="2" />
+            <enum name="boldItalic" value="3" />
+        </attr>
+        <!-- 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="fraction" />
+        <!-- Large size of the text for keys with multiple letters, in the proportion of key height. -->
+        <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="fraction" />
+        <!-- Size of the text for hint label, in the proportion of key height. -->
+        <attr name="keyHintLabelRatio" format="fraction" />
+        <!-- Size of the text for shifted letter hint, in the proportion of key height. -->
+        <attr name="keyShiftedLetterHintRatio" format="fraction" />
+        <!-- Color to use for the label in a key. -->
+        <attr name="keyTextColor" format="color" />
+        <attr name="keyTextShadowColor" format="color" />
+        <!-- Color to use for the label in a key when in inactivated state. -->
+        <attr name="keyTextInactivatedColor" format="color" />
+        <!-- Key hint letter (= one character hint label) color -->
+        <attr name="keyHintLetterColor" format="color" />
+        <!-- Key hint label color -->
+        <attr name="keyHintLabelColor" format="color" />
+        <!-- Shifted letter hint colors -->
+        <attr name="keyShiftedLetterHintInactivatedColor" format="color" />
+        <attr name="keyShiftedLetterHintActivatedColor" format="color" />
+
+        <!-- Key preview visual parameters -->
+        <!-- The text color for key press feedback. -->
+        <attr name="keyPreviewTextColor" format="color" />
+        <!-- Size of the text for key press feedback popup, in the proportion of key height. -->
+        <attr name="keyPreviewTextRatio" format="fraction" />
     </declare-styleable>
 
     <declare-styleable name="Keyboard_Include">
diff --git a/java/res/values/bools.xml b/java/res/values/bools.xml
index 889d8f7..10d2179 100644
--- a/java/res/values/bools.xml
+++ b/java/res/values/bools.xml
@@ -1,24 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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>
     <!-- Whether this input method should be used as the default for a locale. Override it
-         for latin languages. -->
+         for supported languages. -->
     <bool name="im_is_default">false</bool>
 </resources>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 8d3319d..8e2d43e 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -19,16 +19,15 @@
 -->
 
 <resources>
+    <!-- Device form factor. This value must be aligned with {@link KeyboardId.DEVICE_FORM_FACTOR_PHONE} -->
+    <integer name="config_device_form_factor">0</integer>
     <bool name="config_use_fullscreen_mode">false</bool>
     <bool name="config_enable_show_voice_key_option">true</bool>
     <bool name="config_enable_show_popup_on_keypress_option">true</bool>
-    <bool name="config_enable_next_word_suggestions_option">true</bool>
-    <bool name="config_enable_usability_study_mode_option">false</bool>
+    <!-- TODO: Disable the following configuration for production. -->
+    <bool name="config_enable_usability_study_mode_option">true</bool>
     <!-- Whether or not Popup on key press is enabled by default -->
     <bool name="config_default_popup_preview">true</bool>
-    <!-- Default value for next word suggestion: while showing suggestions for a word should we weigh
-         in the previous word? -->
-    <bool name="config_default_next_word_suggestions">true</bool>
     <!-- Default value for next word prediction: after entering a word and a space only, should we look
          at input history to suggest a hopefully helpful suggestions for the next word? -->
     <bool name="config_default_next_word_prediction">true</bool>
@@ -50,8 +49,12 @@
          Configuration for KeyboardView
     -->
     <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 LatinKeyboardView
+         Configuration for MainKeyboardView
     -->
     <dimen name="config_key_hysteresis_distance">8.0dp</dimen>
     <integer name="config_touch_noise_threshold_time">40</integer>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 925eb55..c7d9936 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -1,19 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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.
 */
 -->
@@ -73,6 +73,11 @@
     <dimen name="key_popup_hint_letter_padding">2dp</dimen>
     <dimen name="key_uppercase_letter_padding">2dp</dimen>
 
+    <!-- For 5-row keyboard -->
+    <fraction name="key_bottom_gap_5row">3.20%p</fraction>
+    <fraction name="key_letter_ratio_5row">64%</fraction>
+    <fraction name="key_uppercase_letter_ratio_5row">41%</fraction>
+
     <dimen name="key_preview_offset_ics">8.0dp</dimen>
     <!-- popup_key_height x -0.5 -->
     <dimen name="more_keys_keyboard_vertical_correction_ics">-26.4dp</dimen>
@@ -92,5 +97,18 @@
     <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 trail parameters -->
+    <dimen name="gesture_preview_trail_start_width">12.6dp</dimen>
+    <dimen name="gesture_preview_trail_end_width">2.5dp</dimen>
+    <!-- Gesture floating preview text parameters -->
+    <dimen name="gesture_floating_preview_text_size">24dp</dimen>
+    <dimen name="gesture_floating_preview_text_offset">73dp</dimen>
+    <dimen name="gesture_floating_preview_horizontal_padding">24dp</dimen>
+    <dimen name="gesture_floating_preview_vertical_padding">16dp</dimen>
+    <dimen name="gesture_floating_preview_round_radius">3dp</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/donottranslate.xml b/java/res/values/donottranslate.xml
index 0970aee..9e07b22 100644
--- a/java/res/values/donottranslate.xml
+++ b/java/res/values/donottranslate.xml
@@ -23,9 +23,9 @@
     <!-- Symbols that should be swapped with a weak space -->
     <string name="weak_space_swapping_symbols">.,;:!?)]}\"</string>
     <!-- Symbols that should strip a weak space -->
-    <string name="weak_space_stripping_symbols">"&#x0009;&#x0020;\n/_\'-"</string>
+    <string name="weak_space_stripping_symbols">"&#x0009;&#x0020;\n/_\'-"@</string>
     <!-- Symbols that should convert weak spaces into real space -->
-    <string name="phantom_space_promoting_symbols">([*&amp;@{&lt;&gt;+=|</string>
+    <string name="phantom_space_promoting_symbols">([*&amp;{&lt;&gt;+=|</string>
     <!-- Symbols that do NOT separate words -->
     <string name="symbols_excluded_from_word_separators">\'-</string>
     <!-- Word separator list is the union of all symbols except those that are not separators:
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values/gesture-input.xml
similarity index 78%
copy from java/res/xml/kbd_esperanto.xml
copy to java/res/values/gesture-input.xml
index c0c45dd..235616f 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values/gesture-input.xml
@@ -17,10 +17,6 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <bool name="config_gesture_input_enabled_by_build_config">false</bool>
+</resources>
diff --git a/java/res/values/keypress-vibration-durations.xml b/java/res/values/keypress-vibration-durations.xml
index 2569f23..370959c 100644
--- a/java/res/values/keypress-vibration-durations.xml
+++ b/java/res/values/keypress-vibration-durations.xml
@@ -22,5 +22,7 @@
         <!-- Build.HARDWARE,duration_in_milliseconds -->
         <item>herring,5</item>
         <item>tuna,5</item>
+        <item>mako,5</item>
+        <item>manta,16</item>
     </string-array>
 </resources>
diff --git a/java/res/values/keypress-volumes.xml b/java/res/values/keypress-volumes.xml
index 3b433e4..d112069 100644
--- a/java/res/values/keypress-volumes.xml
+++ b/java/res/values/keypress-volumes.xml
@@ -24,5 +24,7 @@
         <item>tuna,0.5</item>
         <item>stingray,0.4</item>
         <item>grouper,0.3</item>
+        <item>mako,0.3</item>
+        <item>manta,0.2</item>
     </string-array>
 </resources>
diff --git a/java/res/values/predefined-subtypes.xml b/java/res/values/predefined-subtypes.xml
index 602f53e..3bf0e61 100644
--- a/java/res/values/predefined-subtypes.xml
+++ b/java/res/values/predefined-subtypes.xml
@@ -18,6 +18,9 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Predefined subtypes (language:layout[:extraValue]) in semicolon separated format -->
-    <string name="predefined_subtypes" translatable="false">de:qwerty:AsciiCapable;fr:qwertz:AsciiCapable</string>
+    <!-- Predefined subtypes (language:layout[:extraValue]) -->
+    <string-array name="predefined_subtypes" translatable="false">
+        <item>de:qwerty:AsciiCapable</item>
+        <item>fr:qwertz:AsciiCapable</item>
+    </string-array>
 </resources>
diff --git a/java/res/values/whitelist.xml b/java/res/values/research_strings.xml
similarity index 71%
rename from java/res/values/whitelist.xml
rename to java/res/values/research_strings.xml
index d4ecbfa..2cad15e 100644
--- a/java/res/values/whitelist.xml
+++ b/java/res/values/research_strings.xml
@@ -2,7 +2,7 @@
 <!--
 /*
 **
-** Copyright 2011, The Android Open Source Project
+** 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.
@@ -18,12 +18,7 @@
 */
 -->
 <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>
+    <!-- Contents of note explaining what data is collected and how. -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_splash_content" translatable="false"></string>
 </resources>
diff --git a/java/res/values/strings-appname.xml b/java/res/values/strings-appname.xml
new file mode 100644
index 0000000..19aaa25
--- /dev/null
+++ b/java/res/values/strings-appname.xml
@@ -0,0 +1,33 @@
+<?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.
+*/
+-->
+
+<resources>
+    <!-- Title for Latin Keyboard  -->
+    <string name="english_ime_name">Android keyboard</string>
+
+    <!-- Name of Android spell checker service -->
+    <string name="spell_checker_service_name">Android spell checker</string>
+
+    <!-- Title for Latin keyboard settings activity / dialog -->
+    <string name="english_ime_settings">Android keyboard settings</string>
+
+    <!-- Title for the spell checking service settings screen -->
+    <string name="android_spell_checker_settings">Spell checking settings</string>
+</resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index d51d378..392a070 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -18,23 +18,16 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Title for Latin keyboard  -->
-    <string name="english_ime_name">Android keyboard</string>
     <!-- Application name for opensource Android keyboard. AOSP(Android Open Source Project) should not be translated. -->
     <string name="aosp_android_keyboard_ime_name">Android keyboard (AOSP)</string>
-    <!-- Title for Latin keyboard settings activity / dialog -->
-    <string name="english_ime_settings">Android keyboard settings</string>
     <!-- Title for Latin keyboard input options dialog [CHAR LIMIT=25] -->
     <string name="english_ime_input_options">Input options</string>
+    <!-- Title for Latin keyboard research log dialog, which contains special commands for users that contribute data for research. [CHAR LIMIT=33] -->
+    <string name="english_ime_research_log">Research Log Commands</string>
 
-    <!-- Name of Android spell checker service -->
-    <string name="spell_checker_service_name">Android spell checker</string>
     <!-- Name of Android spell checker service. AOSP(Android Open Source Project) should not be translated. -->
     <string name="aosp_spell_checker_service_name">Android spell checker (AOSP)</string>
 
-    <!-- Title for the spell checking service settings screen -->
-    <string name="android_spell_checker_settings">Spell checking settings</string>
-
     <!-- Title for the spell checker option to turn on/off contact names lookup [CHAR LIMIT=25] -->
     <string name="use_contacts_for_spellchecking_option_title">Look up contact names</string>
 
@@ -68,8 +61,10 @@
     <string name="include_other_imes_in_language_switch_list">Switch to other input methods</string>
     <!-- Option summary for including other IMEs in the language switch list [CHAR LIMIT=65] -->
     <string name="include_other_imes_in_language_switch_list_summary">Language switch key covers other input methods too</string>
-    <!-- Option to suppress language switch key [CHAR LIMIT=30] -->
-    <string name="suppress_language_switch_key">Suppress language switch key</string>
+    <!-- Option to show language switch key [CHAR LIMIT=30] -->
+    <string name="show_language_switch_key">Language switch key</string>
+    <!-- Option summary for showing language switch key [CHAR LIMIT=65] -->
+    <string name="show_language_switch_key_summary">Show when multiple input languages are enabled</string>
 
     <!-- Option for the dismiss delay of the key popup [CHAR LIMIT=25] -->
     <string name="key_preview_popup_dismiss_delay">Key popup dismiss delay</string>
@@ -83,11 +78,6 @@
     <!-- Description for option enabling or disabling the use of names of people in Contacts for suggestion and correction [CHAR LIMIT=65] -->
     <string name="use_contacts_dict_summary">Use names from Contacts for suggestions and corrections</string>
 
-    <!-- Option name for enabling insertion of suggestion spans (advanced option) [CHAR LIMIT=25] -->
-    <string name="enable_span_insert">Enable recorrections</string>
-    <!-- Option summary for enabling insertion of suggestion spans (advanced option) [CHAR LIMIT=65] -->
-    <string name="enable_span_insert_summary">Set suggestions for recorrections</string>
-
     <!-- Option to enable auto capitalization of sentences -->
     <string name="auto_cap">Auto-capitalization</string>
 
@@ -118,14 +108,21 @@
     <!-- Option to suggest auto correction suggestions very aggressively. Auto-corrects to a word which has even large edit distance from typed word. [CHAR LIMIT=20] -->
     <string name="auto_correction_threshold_mode_very_aggeressive">Very aggressive</string>
 
-    <!-- Option to enable next word correction -->
-    <string name="bigram_suggestion">Next word suggestions</string>
-    <!-- Option to enable next word suggestion. This uses the previous word in an attempt to improve the suggestions quality -->
-    <string name="bigram_suggestion_summary">Use previous word to improve suggestions</string>
-    <!-- Option to enable using next word prediction -->
-    <string name="bigram_prediction">Next word prediction</string>
-    <!-- Description for "next word prediction" option. This displays suggestions even when there is no input, based on the previous word. -->
-    <string name="bigram_prediction_summary">Use previous word also for prediction</string>
+    <!-- Option to enable using next word suggestions. After the user types a space, with this option on, the keyboard will try to predict the next word. -->
+    <string name="bigram_prediction">Next word suggestions</string>
+    <!-- Description for "next word suggestion" option. This displays suggestions even when there is no input, based on the previous word. -->
+    <string name="bigram_prediction_summary">Based on previous word</string>
+
+    <!-- Option to enable gesture input. The user can input a word by tracing the letters of a word without releasing the finger from the screen. [CHAR LIMIT=30]-->
+    <string name="gesture_input">Gesture input</string>
+    <!-- Description for "gesture_input" option. The user can input a word by tracing the letters of a word without releasing the finger from the screen. [CHAR LIMIT=65]-->
+    <string name="gesture_input_summary">Input a word by tracing the letters of a word</string>
+    <!-- Option to enable gesture trail preview. The user can see a trail of the gesture during gesture input. [CHAR LIMIT=30]-->
+    <string name="gesture_preview_trail">Show gesture trail</string>
+    <!-- Option to enable gesture floating text preview. The user can see a suggested word floating under the moving finger during a gesture input. [CHAR LIMIT=30]-->
+    <string name="gesture_floating_preview_text">Show gesture word</string>
+    <!-- Description for "gesture_floating_preview_text" option. The user can see a suggested word floating under the moving finger during a gesture input. [CHAR LIMIT=65]-->
+    <string name="gesture_floating_preview_text_summary">Show floating preview word with gesture</string>
 
     <!-- Indicates that a word has been added to the dictionary -->
     <string name="added_word"><xliff:g id="word">%s</xliff:g> : Saved</string>
@@ -192,6 +189,12 @@
     <string name="spoken_description_search">Search</string>
     <!-- Spoken description for the "U+2022" (BULLET) keyboard key. -->
     <string name="spoken_description_dot">Dot</string>
+    <!-- Spoken description for the "Switch language" keyboard key. -->
+    <string name="spoken_description_language_switch">Switch language</string>
+    <!-- Spoken description for the "Next" action keyboard key. -->
+    <string name="spoken_description_action_next">Next</string>
+    <!-- Spoken description for the "Previous" action keyboard key. -->
+    <string name="spoken_description_action_previous">Previous</string>
 
     <!-- Spoken feedback after turning "Shift" mode on. -->
     <string name="spoken_description_shiftmode_on">Shift enabled</string>
@@ -233,6 +236,60 @@
     <!-- Title for input language selection screen -->
     <string name="language_selection_title">Input languages</string>
 
+    <!-- Title for dialog option to let users cancel logging and delete log for this session [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_do_not_log_this_session" translatable="false">Suspend logging</string>
+    <!-- Title for dialog option to let users reenable logging [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_enable_session_logging" translatable="false">Enable logging</string>
+    <!-- Toast notification that the system is processing the request to delete the log for this session [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_notify_session_log_deleting" translatable="false">Deleting session log</string>
+    <!-- Toast notification that the system has successfully deleted the log for this session [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_notify_logging_suspended" translatable="false">Logging temporarily suspended.  To disable permanently, go to Android Keyboard Settings</string>
+    <!-- Toast notification that the system has failed to delete the log for this session [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_notify_session_log_not_deleted" translatable="false">Session log NOT deleted</string>
+    <!-- Toast notification that the system is enabling logging [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_notify_session_logging_enabled" translatable="false">Session logging enabled</string>
+
+    <!-- Menu option that lets user send feedback for research purposes about the IME [CHAR LIMIT=38] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_menu_option" translatable="false">Send feedback</string>
+    <!-- Dialog box title that lets user send feedback for research purposes about the IME [CHAR LIMIT=38] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <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 -->
+    <!-- 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>
+    <!-- Dialog button choice to send research feedback [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_send" translatable="false">Send</string>
+    <!-- Dialog button choice to cancel sending research feedback [CHAR LIMIT=35] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_cancel" translatable="false">Cancel</string>
+    <!-- Toast notification to ask user to quit the research feedback dialog to perform this operation [CHAR LIMIT=100] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_please_exit_feedback_form" translatable="false">Please exit the feedback dialog to access the research log menu</string>
+
+    <!-- Title of dialog shown at start informing users about contributing research usage data-->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_splash_title" translatable="false">Warning</string>
+
+    <!-- Toast message informing users that logging has been disabled -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_logging_disabled" translatable="false">Logging Disabled</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>
 
@@ -260,6 +317,15 @@
     <!-- Description for English (United States) keyboard subtype with explicit keyboard layout [CHAR LIMIT=25]
          This should be identical to subtype_en_US aside from the trailing (%s). -->
     <string name="subtype_with_layout_en_US">English (US) (<xliff:g id="layout">%s</xliff:g>)</string>
+    <!-- TODO: Uncomment once we can handle IETF language tag with script name specified.
+         Description for Serbian Cyrillic keyboard subtype [CHAR LIMIT=25]
+    <string name="subtype_serbian_cyrillic">Serbian (Cyrillic)</string>
+         Description for Serbian Latin keyboard subtype [CHAR LIMIT=25]
+    <string name="subtype_serbian_latin">Serbian (Latin)</string>
+         Description for Serbian Latin keyboard subtype with explicit keyboard layout [CHAR LIMIT=25]
+         This should be identical to subtype_serbian_latin aside from the trailing (%s).
+    <string name="subtype_with_layout_sr-Latn">Serbian (Latin) (<xliff:g id="layout">%s</xliff:g>)</string>
+    -->
     <!-- Description for language agnostic keyboard subtype [CHAR LIMIT=25] -->
     <string name="subtype_no_language">No language</string>
     <!-- Description for language agnostic QWERTY keyboard subtype [CHAR LIMIT=25] -->
@@ -289,7 +355,7 @@
     <string name="subtype_locale">Language</string>
     <!-- Title of the spinner for choosing a keyboard layout of custom style in the settings dialog [CHAR LIMIT=15] -->
     <string name="keyboard_layout_set">Layout</string>
-    <!-- The message of the dialog to note that a custom input style needs to be enabled. [CHAR LIMIT=64] -->
+    <!-- The message of the dialog to note that a custom input style needs to be enabled. [CHAR LIMIT=130] -->
     <string name="custom_input_style_note_message">"Your custom input style needs to be enabled before you start using it. Do you want to enable it now?"</string>
     <!-- Title of the button to enable a custom input style entry in the settings dialog [CHAR LIMIT=15] -->
     <string name="enable">Enable</string>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index e9b0470..ed92440 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- Theme "Basic" -->
     <style name="Keyboard">
         <!-- This should be aligned with KeyboardSwitcher.KEYBOARD_THEMES[] -->
@@ -35,14 +35,14 @@
     <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>
         <item name="keyShiftedLetterHintRatio">@fraction/key_uppercase_letter_ratio</item>
-        <item name="keyTextStyle">normal</item>
+        <item name="keyTypeface">normal</item>
         <item name="keyTextColor">#FFFFFFFF</item>
         <item name="keyTextInactivatedColor">#FFFFFFFF</item>
         <item name="keyHintLetterColor">#80000000</item>
@@ -54,9 +54,6 @@
         <item name="keyPopupHintLetterPadding">@dimen/key_popup_hint_letter_padding</item>
         <item name="keyShiftedLetterHintPadding">@dimen/key_uppercase_letter_padding</item>
         <item name="keyPreviewLayout">@layout/key_preview</item>
-        <item name="keyPreviewBackground">@drawable/keyboard_key_feedback</item>
-        <item name="keyPreviewLeftBackground">@null</item>
-        <item name="keyPreviewRightBackground">@null</item>
         <item name="keyPreviewTextColor">#FFFFFFFF</item>
         <item name="keyPreviewOffset">@dimen/key_preview_offset</item>
         <item name="keyPreviewHeight">@dimen/key_preview_height</item>
@@ -64,10 +61,25 @@
         <item name="keyPreviewLingerTimeout">@integer/config_key_preview_linger_timeout</item>
         <item name="moreKeysLayout">@layout/more_keys_keyboard</item>
         <item name="verticalCorrection">@dimen/keyboard_vertical_correction</item>
-        <item name="shadowColor">#BB000000</item>
-        <item name="shadowRadius">2.75</item>
+        <item name="keyTextShadowColor">#BB000000</item>
+        <item name="keyTextShadowRadius">2.75</item>
         <item name="backgroundDimAlpha">128</item>
-        <!-- Common attributes of LatinKeyboardView -->
+        <!-- android:color/holo_blue_light=#FF33B5E5 -->
+        <item name="gestureFloatingPreviewTextSize">@dimen/gesture_floating_preview_text_size</item>
+        <item name="gestureFloatingPreviewTextColor">@android:color/holo_blue_light</item>
+        <item name="gestureFloatingPreviewTextOffset">@dimen/gesture_floating_preview_text_offset</item>
+        <item name="gestureFloatingPreviewColor">#C0000000</item>
+        <item name="gestureFloatingPreviewHorizontalPadding">@dimen/gesture_floating_preview_horizontal_padding</item>
+        <item name="gestureFloatingPreviewVerticalPadding">@dimen/gesture_floating_preview_vertical_padding</item>
+        <item name="gestureFloatingPreviewRoundRadius">@dimen/gesture_floating_preview_round_radius</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="gesturePreviewTrailStartWidth">@dimen/gesture_preview_trail_start_width</item>
+        <item name="gesturePreviewTrailEndWidth">@dimen/gesture_preview_trail_end_width</item>
+        <!-- Common attributes of MainKeyboardView -->
         <item name="keyHysteresisDistance">@dimen/config_key_hysteresis_distance</item>
         <item name="touchNoiseThresholdTime">@integer/config_touch_noise_threshold_time</item>
         <item name="touchNoiseThresholdDistance">@dimen/config_touch_noise_threshold_distance</item>
@@ -84,7 +96,7 @@
         <item name="altCodeKeyWhileTypingFadeinAnimator">@anim/alt_code_key_while_typing_fadein</item>
     </style>
     <style
-        name="LatinKeyboardView"
+        name="MainKeyboardView"
         parent="KeyboardView">
         <item name="autoCorrectionSpacebarLedEnabled">true</item>
         <item name="autoCorrectionSpacebarLedIcon">@drawable/sym_keyboard_space_led</item>
@@ -114,7 +126,7 @@
         <item name="android:background">@drawable/keyboard_suggest_strip</item>
     </style>
     <style
-        name="SuggestionsViewStyle"
+        name="SuggestionStripViewStyle"
         parent="SuggestionsStripBackgroundStyle"
     >
         <item name="suggestionStripOption">autoCorrectBold|validTypedWordBold</item>
@@ -122,9 +134,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>
@@ -155,7 +167,7 @@
         <item name="keyBackground">@drawable/btn_keyboard_key3</item>
     </style>
     <style
-        name="LatinKeyboardView.HighContrast"
+        name="MainKeyboardView.HighContrast"
         parent="KeyboardView.HighContrast"
     >
         <item name="autoCorrectionSpacebarLedEnabled">true</item>
@@ -187,10 +199,10 @@
         <item name="keyHintLabelColor">#E0000000</item>
         <item name="keyShiftedLetterHintInactivatedColor">#66000000</item>
         <item name="keyShiftedLetterHintActivatedColor">#CC000000</item>
-        <item name="shadowColor">#FFFFFFFF</item>
+        <item name="keyTextShadowColor">#FFFFFFFF</item>
     </style>
     <style
-        name="LatinKeyboardView.Stone"
+        name="MainKeyboardView.Stone"
         parent="KeyboardView.Stone"
     >
         <item name="autoCorrectionSpacebarLedEnabled">true</item>
@@ -213,7 +225,7 @@
     >
         <item name="keyBackground">@drawable/btn_keyboard_key_stone</item>
         <item name="keyTextColor">#FF000000</item>
-        <item name="shadowColor">#FFFFFFFF</item>
+        <item name="keyTextShadowColor">#FFFFFFFF</item>
     </style>
     <!-- Theme "Stone bold" -->
     <style
@@ -227,10 +239,10 @@
         name="KeyboardView.Stone.Bold"
         parent="KeyboardView.Stone"
     >
-        <item name="keyTextStyle">bold</item>
+        <item name="keyTypeface">bold</item>
     </style>
     <style
-        name="LatinKeyboardView.Stone.Bold"
+        name="MainKeyboardView.Stone.Bold"
         parent="KeyboardView.Stone.Bold"
     >
         <item name="autoCorrectionSpacebarLedEnabled">true</item>
@@ -256,10 +268,10 @@
     >
         <item name="android:background">@drawable/keyboard_dark_background</item>
         <item name="keyBackground">@drawable/btn_keyboard_key_gingerbread</item>
-        <item name="keyTextStyle">bold</item>
+        <item name="keyTypeface">bold</item>
     </style>
     <style
-        name="LatinKeyboardView.Gingerbread"
+        name="MainKeyboardView.Gingerbread"
         parent="KeyboardView.Gingerbread"
     >
         <item name="autoCorrectionSpacebarLedEnabled">true</item>
@@ -301,22 +313,20 @@
     >
         <item name="android:background">@drawable/keyboard_background_holo</item>
         <item name="keyBackground">@drawable/btn_keyboard_key_ics</item>
-        <item name="keyTextStyle">bold</item>
+        <item name="keyTypeface">bold</item>
         <item name="keyTextInactivatedColor">#66E0E4E5</item>
         <item name="keyHintLetterColor">#80000000</item>
         <item name="keyHintLabelColor">#A0FFFFFF</item>
         <item name="keyShiftedLetterHintInactivatedColor">#66E0E4E5</item>
         <item name="keyShiftedLetterHintActivatedColor">#FFFFFFFF</item>
-        <item name="keyPreviewBackground">@drawable/keyboard_key_feedback_ics</item>
-        <item name="keyPreviewLeftBackground">@drawable/keyboard_key_feedback_left_ics</item>
-        <item name="keyPreviewRightBackground">@drawable/keyboard_key_feedback_right_ics</item>
+        <item name="keyPreviewLayout">@layout/key_preview_ics</item>
         <item name="keyPreviewTextColor">#FFFFFFFF</item>
         <item name="keyPreviewOffset">@dimen/key_preview_offset_ics</item>
-        <item name="shadowColor">#00000000</item>
-        <item name="shadowRadius">0.0</item>
+        <item name="keyTextShadowColor">#00000000</item>
+        <item name="keyTextShadowRadius">0.0</item>
     </style>
     <style
-        name="LatinKeyboardView.IceCreamSandwich"
+        name="MainKeyboardView.IceCreamSandwich"
         parent="KeyboardView.IceCreamSandwich"
     >
         <item name="autoCorrectionSpacebarLedEnabled">false</item>
@@ -348,7 +358,7 @@
         <item name="android:background">@drawable/keyboard_suggest_strip_holo</item>
     </style>
     <style
-        name="SuggestionsViewStyle.IceCreamSandwich"
+        name="SuggestionStripViewStyle.IceCreamSandwich"
         parent="SuggestionsStripBackgroundStyle.IceCreamSandwich"
     >
         <item name="suggestionStripOption">autoCorrectBold|validTypedWordBold</item>
@@ -357,12 +367,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/themes-basic-highcontrast.xml b/java/res/values/themes-basic-highcontrast.xml
index 19df42c..b3ea050 100644
--- a/java/res/values/themes-basic-highcontrast.xml
+++ b/java/res/values/themes-basic-highcontrast.xml
@@ -18,12 +18,12 @@
     <style name="KeyboardTheme.HighContrast" parent="KeyboardIcons">
         <item name="keyboardStyle">@style/Keyboard.HighContrast</item>
         <item name="keyboardViewStyle">@style/KeyboardView.HighContrast</item>
-        <item name="latinKeyboardViewStyle">@style/LatinKeyboardView.HighContrast</item>
+        <item name="mainKeyboardViewStyle">@style/MainKeyboardView.HighContrast</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView</item>
         <item name="moreKeysKeyboardPanelStyle">@style/MoreKeysKeyboardPanelStyle</item>
         <item name="suggestionsStripBackgroundStyle">@style/SuggestionsStripBackgroundStyle</item>
-        <item name="suggestionsViewStyle">@style/SuggestionsViewStyle</item>
+        <item name="suggestionStripViewStyle">@style/SuggestionStripViewStyle</item>
         <item name="moreSuggestionsViewStyle">@style/MoreSuggestionsViewStyle</item>
         <item name="suggestionBackgroundStyle">@style/SuggestionBackgroundStyle</item>
         <item name="suggestionPreviewBackgroundStyle">@style/SuggestionPreviewBackgroundStyle</item>
diff --git a/java/res/values/themes-basic.xml b/java/res/values/themes-basic.xml
index 5d47720..ff6a70a 100644
--- a/java/res/values/themes-basic.xml
+++ b/java/res/values/themes-basic.xml
@@ -18,12 +18,12 @@
     <style name="KeyboardTheme" parent="KeyboardIcons">
         <item name="keyboardStyle">@style/Keyboard</item>
         <item name="keyboardViewStyle">@style/KeyboardView</item>
-        <item name="latinKeyboardViewStyle">@style/LatinKeyboardView</item>
+        <item name="mainKeyboardViewStyle">@style/MainKeyboardView</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView</item>
         <item name="moreKeysKeyboardPanelStyle">@style/MoreKeysKeyboardPanelStyle</item>
         <item name="suggestionsStripBackgroundStyle">@style/SuggestionsStripBackgroundStyle</item>
-        <item name="suggestionsViewStyle">@style/SuggestionsViewStyle</item>
+        <item name="suggestionStripViewStyle">@style/SuggestionStripViewStyle</item>
         <item name="moreSuggestionsViewStyle">@style/MoreSuggestionsViewStyle</item>
         <item name="suggestionBackgroundStyle">@style/SuggestionBackgroundStyle</item>
         <item name="suggestionPreviewBackgroundStyle">@style/SuggestionPreviewBackgroundStyle</item>
diff --git a/java/res/values/themes-gingerbread.xml b/java/res/values/themes-gingerbread.xml
index a139798..0ce0b8a 100644
--- a/java/res/values/themes-gingerbread.xml
+++ b/java/res/values/themes-gingerbread.xml
@@ -18,12 +18,12 @@
     <style name="KeyboardTheme.Gingerbread" parent="KeyboardIcons">
         <item name="keyboardStyle">@style/Keyboard.Gingerbread</item>
         <item name="keyboardViewStyle">@style/KeyboardView.Gingerbread</item>
-        <item name="latinKeyboardViewStyle">@style/LatinKeyboardView.Gingerbread</item>
+        <item name="mainKeyboardViewStyle">@style/MainKeyboardView.Gingerbread</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.Gingerbread</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.Gingerbread</item>
         <item name="moreKeysKeyboardPanelStyle">@style/MoreKeysKeyboardPanelStyle</item>
         <item name="suggestionsStripBackgroundStyle">@style/SuggestionsStripBackgroundStyle</item>
-        <item name="suggestionsViewStyle">@style/SuggestionsViewStyle</item>
+        <item name="suggestionStripViewStyle">@style/SuggestionStripViewStyle</item>
         <item name="moreSuggestionsViewStyle">@style/MoreSuggestionsViewStyle</item>
         <item name="suggestionBackgroundStyle">@style/SuggestionBackgroundStyle</item>
         <item name="suggestionPreviewBackgroundStyle">@style/SuggestionPreviewBackgroundStyle</item>
diff --git a/java/res/values/themes-ics.xml b/java/res/values/themes-ics.xml
index e6fd4f4..8df58c5 100644
--- a/java/res/values/themes-ics.xml
+++ b/java/res/values/themes-ics.xml
@@ -18,12 +18,12 @@
     <style name="KeyboardTheme.IceCreamSandwich" parent="KeyboardIcons.IceCreamSandwich">
         <item name="keyboardStyle">@style/Keyboard.IceCreamSandwich</item>
         <item name="keyboardViewStyle">@style/KeyboardView.IceCreamSandwich</item>
-        <item name="latinKeyboardViewStyle">@style/LatinKeyboardView.IceCreamSandwich</item>
+        <item name="mainKeyboardViewStyle">@style/MainKeyboardView.IceCreamSandwich</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.IceCreamSandwich</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.IceCreamSandwich</item>
         <item name="moreKeysKeyboardPanelStyle">@style/MoreKeysKeyboardPanelStyle.IceCreamSandwich</item>
         <item name="suggestionsStripBackgroundStyle">@style/SuggestionsStripBackgroundStyle.IceCreamSandwich</item>
-        <item name="suggestionsViewStyle">@style/SuggestionsViewStyle.IceCreamSandwich</item>
+        <item name="suggestionStripViewStyle">@style/SuggestionStripViewStyle.IceCreamSandwich</item>
         <item name="moreSuggestionsViewStyle">@style/MoreSuggestionsViewStyle.IceCreamSandwich</item>
         <item name="suggestionBackgroundStyle">@style/SuggestionBackgroundStyle.IceCreamSandwich</item>
         <item name="suggestionPreviewBackgroundStyle">@style/SuggestionPreviewBackgroundStyle.IceCreamSandwich</item>
diff --git a/java/res/values/themes-stone-bold.xml b/java/res/values/themes-stone-bold.xml
index 47de99e..355a97f 100644
--- a/java/res/values/themes-stone-bold.xml
+++ b/java/res/values/themes-stone-bold.xml
@@ -18,12 +18,12 @@
     <style name="KeyboardTheme.Stone.Bold" parent="KeyboardIcons.Black">
         <item name="keyboardStyle">@style/Keyboard.Stone.Bold</item>
         <item name="keyboardViewStyle">@style/KeyboardView.Stone.Bold</item>
-        <item name="latinKeyboardViewStyle">@style/LatinKeyboardView.Stone.Bold</item>
+        <item name="mainKeyboardViewStyle">@style/MainKeyboardView.Stone.Bold</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.Stone</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.Stone</item>
         <item name="moreKeysKeyboardPanelStyle">@style/MoreKeysKeyboardPanelStyle</item>
         <item name="suggestionsStripBackgroundStyle">@style/SuggestionsStripBackgroundStyle</item>
-        <item name="suggestionsViewStyle">@style/SuggestionsViewStyle</item>
+        <item name="suggestionStripViewStyle">@style/SuggestionStripViewStyle</item>
         <item name="moreSuggestionsViewStyle">@style/MoreSuggestionsViewStyle</item>
         <item name="suggestionBackgroundStyle">@style/SuggestionBackgroundStyle</item>
         <item name="suggestionPreviewBackgroundStyle">@style/SuggestionPreviewBackgroundStyle</item>
diff --git a/java/res/values/themes-stone.xml b/java/res/values/themes-stone.xml
index a0b39e3..23437f7 100644
--- a/java/res/values/themes-stone.xml
+++ b/java/res/values/themes-stone.xml
@@ -18,12 +18,12 @@
     <style name="KeyboardTheme.Stone" parent="KeyboardIcons.Black">
         <item name="keyboardStyle">@style/Keyboard.Stone</item>
         <item name="keyboardViewStyle">@style/KeyboardView.Stone</item>
-        <item name="latinKeyboardViewStyle">@style/LatinKeyboardView.Stone</item>
+        <item name="mainKeyboardViewStyle">@style/MainKeyboardView.Stone</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.Stone</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.Stone</item>
         <item name="moreKeysKeyboardPanelStyle">@style/MoreKeysKeyboardPanelStyle</item>
         <item name="suggestionsStripBackgroundStyle">@style/SuggestionsStripBackgroundStyle</item>
-        <item name="suggestionsViewStyle">@style/SuggestionsViewStyle</item>
+        <item name="suggestionStripViewStyle">@style/SuggestionStripViewStyle</item>
         <item name="moreSuggestionsViewStyle">@style/MoreSuggestionsViewStyle</item>
         <item name="suggestionBackgroundStyle">@style/SuggestionBackgroundStyle</item>
         <item name="suggestionPreviewBackgroundStyle">@style/SuggestionPreviewBackgroundStyle</item>
diff --git a/java/res/xml/kbd_esperanto.xml b/java/res/values/urls.xml
similarity index 78%
rename from java/res/xml/kbd_esperanto.xml
rename to java/res/values/urls.xml
index c0c45dd..a8e9ad7 100644
--- a/java/res/xml/kbd_esperanto.xml
+++ b/java/res/values/urls.xml
@@ -17,10 +17,6 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/rows_esperanto" />
-</Keyboard>
+<resources>
+    <string name="research_logger_upload_url" translatable="false"></string>
+</resources>
diff --git a/java/res/xml-sw600dp-land/kbd_thai.xml b/java/res/xml-sw600dp-land/kbd_thai.xml
deleted file mode 100644
index b75980f..0000000
--- a/java/res/xml-sw600dp-land/kbd_thai.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
-*/
--->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:rowHeight="20%p"
-    latin:verticalGap="3.20%p"
-    latin:touchPositionCorrectionData="@null"
->
-    <include
-        latin:keyboardLayout="@xml/rows_thai" />
-</Keyboard>
diff --git a/java/res/xml-sw600dp/kbd_thai.xml b/java/res/xml-sw600dp/kbd_thai.xml
deleted file mode 100644
index b75980f..0000000
--- a/java/res/xml-sw600dp/kbd_thai.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
-*/
--->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:rowHeight="20%p"
-    latin:verticalGap="3.20%p"
-    latin:touchPositionCorrectionData="@null"
->
-    <include
-        latin:keyboardLayout="@xml/rows_thai" />
-</Keyboard>
diff --git a/java/res/xml-sw600dp/key_styles_common.xml b/java/res/xml-sw600dp/key_styles_common.xml
index a1b2eb4..bf2e76a 100644
--- a/java/res/xml-sw600dp/key_styles_common.xml
+++ b/java/res/xml-sw600dp/key_styles_common.xml
@@ -133,6 +133,17 @@
                 latin:keyIconPreview="!icon/tab_key_preview"
                 latin:backgroundType="functional" />
         </case>
+        <case
+            latin:keyboardLayoutSetElement="alphabet|alphabetAutomaticShifted|alphabetShiftLocked"
+            latin:navigateNext="true"
+        >
+            <key-style
+                latin:styleName="tabKeyStyle"
+                latin:code="!code/key_action_next"
+                latin:keyIcon="!icon/tab_key"
+                latin:keyIconPreview="!icon/tab_key_preview"
+                latin:backgroundType="functional" />
+        </case>
         <default>
             <key-style
                 latin:styleName="tabKeyStyle"
diff --git a/java/res/xml-sw600dp/row_symbols4.xml b/java/res/xml-sw600dp/row_symbols4.xml
index 73a5b17..f138d8e 100644
--- a/java/res/xml-sw600dp/row_symbols4.xml
+++ b/java/res/xml-sw600dp/row_symbols4.xml
@@ -41,6 +41,8 @@
             latin:moreKeys="!text/more_keys_for_tablet_double_quote" />
         <Key
             latin:keyLabel="_" />
-        <!-- Here is empty space. -->
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw600dp/row_symbols_shift4.xml b/java/res/xml-sw600dp/row_symbols_shift4.xml
index 6f3aac7..29befa9 100644
--- a/java/res/xml-sw600dp/row_symbols_shift4.xml
+++ b/java/res/xml-sw600dp/row_symbols_shift4.xml
@@ -33,6 +33,8 @@
             latin:keyXPos="28.0%p"
             latin:keyboardLayout="@xml/key_space"
             latin:backgroundType="normal" />
-        <!-- Here is empty space. -->
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw600dp/rowkeys_arabic1.xml b/java/res/xml-sw600dp/rowkeys_arabic1.xml
index 44fdc67..6a0e257 100644
--- a/java/res/xml-sw600dp/rowkeys_arabic1.xml
+++ b/java/res/xml-sw600dp/rowkeys_arabic1.xml
@@ -23,19 +23,23 @@
 >
     <!-- U+0636: "ض" ARABIC LETTER DAD -->
     <Key
-        latin:keyLabel="&#x0636;" />
+        latin:keyLabel="&#x0636;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0635: "ص" ARABIC LETTER SAD -->
     <Key
-        latin:keyLabel="&#x0635;" />
+        latin:keyLabel="&#x0635;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062B: "ث" ARABIC LETTER THEH -->
     <Key
-        latin:keyLabel="&#x062B;" />
+        latin:keyLabel="&#x062B;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0642: "ق" ARABIC LETTER QAF
          U+06A8: "ڨ" ARABIC LETTER QAF WITH THREE DOTS ABOVE -->
     <!-- TODO: DroidSansArabic lacks the glyph of U+06A8 ARABIC LETTER QAF WITH THREE DOTS ABOVE -->
     <Key
         latin:keyLabel="&#x0642;"
-        latin:moreKeys="&#x06A8;" />
+        latin:moreKeys="&#x06A8;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0641: "ف" ARABIC LETTER FEH
          U+06A4: "ڤ" ARABIC LETTER VEH
          U+06A2: "ڢ" ARABIC LETTER FEH WITH DOT MOVED BELOW
@@ -44,28 +48,35 @@
     <!-- TODO: DroidSansArabic lacks the glyph of U+06A5 ARABIC LETTER FEH WITH THREE DOTS BELOW -->
     <Key
         latin:keyLabel="&#x0641;"
-        latin:moreKeys="&#x06A4;,&#x06A2;,&#x06A5;" />
+        latin:moreKeys="&#x06A4;,&#x06A2;,&#x06A5;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+063A: "غ" ARABIC LETTER GHAIN -->
     <Key
-        latin:keyLabel="&#x063A;" />
+        latin:keyLabel="&#x063A;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0639: "ع" ARABIC LETTER AIN -->
     <Key
-        latin:keyLabel="&#x0639;" />
+        latin:keyLabel="&#x0639;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0647: "ه" ARABIC LETTER HEH
          U+FEEB: "ﻫ" ARABIC LETTER HEH INITIAL FORM
          U+0647 U+200D: ARABIC LETTER HEH + ZERO WIDTH JOINER -->
     <Key
         latin:keyLabel="&#x0647;"
-        latin:moreKeys="&#xFEEB;|&#x0647;&#x200D;" />
+        latin:moreKeys="&#xFEEB;|&#x0647;&#x200D;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062E: "خ" ARABIC LETTER KHAH -->
     <Key
-        latin:keyLabel="&#x062E;" />
+        latin:keyLabel="&#x062E;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062D: "ح" ARABIC LETTER HAH -->
     <Key
-        latin:keyLabel="&#x062D;" />
+        latin:keyLabel="&#x062D;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062C: "ج" ARABIC LETTER JEEM
          U+0686: "چ" ARABIC LETTER TCHEH -->
     <Key
         latin:keyLabel="&#x062C;"
-        latin:moreKeys="&#x0686;" />
+        latin:moreKeys="&#x0686;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml-sw600dp/rowkeys_arabic2.xml b/java/res/xml-sw600dp/rowkeys_arabic2.xml
index 3eba2fb..00e69ac 100644
--- a/java/res/xml-sw600dp/rowkeys_arabic2.xml
+++ b/java/res/xml-sw600dp/rowkeys_arabic2.xml
@@ -26,21 +26,25 @@
     <!-- TODO: DroidSansArabic lacks the glyph of U+069C ARABIC LETTER SEEN WITH THREE DOTS BELOW AND THREE DOTS ABOVE -->
     <Key
         latin:keyLabel="&#x0634;"
-        latin:moreKeys="&#x069C;" />
+        latin:moreKeys="&#x069C;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0633: "س" ARABIC LETTER SEEN -->
     <Key
-        latin:keyLabel="&#x0633;" />
+        latin:keyLabel="&#x0633;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+064A: "ي" ARABIC LETTER YEH
          U+0626: "ئ" ARABIC LETTER YEH WITH HAMZA ABOVE
          U+0649: "ى" ARABIC LETTER ALEF MAKSURA -->
     <Key
         latin:keyLabel="&#x064A;"
-        latin:moreKeys="&#x0626;,&#x0649;" />
+        latin:moreKeys="&#x0626;,&#x0649;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0628: "ب" ARABIC LETTER BEH
          U+067E: "پ" ARABIC LETTER PEH -->
     <Key
         latin:keyLabel="&#x0628;"
-        latin:moreKeys="&#x067E;" />
+        latin:moreKeys="&#x067E;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0644: "ل" ARABIC LETTER LAM
          U+FEFB: "ﻻ" ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM
          U+0627: "ا" ARABIC LETTER ALEF
@@ -52,7 +56,8 @@
          U+0622: "آ" ARABIC LETTER ALEF WITH MADDA ABOVE -->
     <Key
         latin:keyLabel="&#x0644;"
-        latin:moreKeys="&#xFEFB;|&#x0644;&#x0627;,&#xFEF7;|&#x0644;&#x0623;,&#xFEF9;|&#x0644;&#x0625;,&#xFEF5;|&#x0644;&#x0622;" />
+        latin:moreKeys="&#xFEFB;|&#x0644;&#x0627;,&#xFEF7;|&#x0644;&#x0623;,&#xFEF9;|&#x0644;&#x0625;,&#xFEF5;|&#x0644;&#x0622;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0627: "ا" ARABIC LETTER ALEF
          U+0621: "ء" ARABIC LETTER HAMZA
          U+0671: "ٱ" ARABIC LETTER ALEF WASLA
@@ -61,23 +66,29 @@
          U+0622: "آ" ARABIC LETTER ALEF WITH MADDA ABOVE -->
     <Key
         latin:keyLabel="&#x0627;"
-        latin:moreKeys="&#x0621;,&#x0671;,&#x0623;,&#x0625;,&#x0622;" />
+        latin:moreKeys="&#x0621;,&#x0671;,&#x0623;,&#x0625;,&#x0622;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062A: "ت" ARABIC LETTER TEH -->
     <Key
-        latin:keyLabel="&#x062A;" />
+        latin:keyLabel="&#x062A;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0646: "ن" ARABIC LETTER NOON -->
     <Key
-        latin:keyLabel="&#x0646;" />
+        latin:keyLabel="&#x0646;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0645: "م" ARABIC LETTER MEEM -->
     <Key
-        latin:keyLabel="&#x0645;" />
+        latin:keyLabel="&#x0645;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0643: "ك" ARABIC LETTER KAF
          U+06AF: "گ" ARABIC LETTER GAF
          U+06A9: "ک" ARABIC LETTER KEHEH -->
     <Key
         latin:keyLabel="&#x0643;"
-        latin:moreKeys="&#x06AF;,&#x06A9;" />
+        latin:moreKeys="&#x06AF;,&#x06A9;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0637: "ط" ARABIC LETTER TAH -->
     <Key
-        latin:keyLabel="&#x0637;" />
+        latin:keyLabel="&#x0637;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml-sw600dp/rowkeys_arabic3.xml b/java/res/xml-sw600dp/rowkeys_arabic3.xml
index 911550f..b0bcd78 100644
--- a/java/res/xml-sw600dp/rowkeys_arabic3.xml
+++ b/java/res/xml-sw600dp/rowkeys_arabic3.xml
@@ -23,37 +23,48 @@
 >
     <!-- U+0626: "ئ" ARABIC LETTER YEH WITH HAMZA ABOVE -->
     <Key
-        latin:keyLabel="&#x0626;" />
+        latin:keyLabel="&#x0626;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0621: "ء" ARABIC LETTER HAMZA -->
     <Key
-        latin:keyLabel="&#x0621;" />
+        latin:keyLabel="&#x0621;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0624: "ؤ" ARABIC LETTER WAW WITH HAMZA ABOVE -->
     <Key
-        latin:keyLabel="&#x0624;" />
+        latin:keyLabel="&#x0624;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0631: "ر" ARABIC LETTER REH -->
     <Key
-        latin:keyLabel="&#x0631;" />
+        latin:keyLabel="&#x0631;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0630: "ذ" ARABIC LETTER THAL -->
     <Key
-        latin:keyLabel="&#x0630;" />
+        latin:keyLabel="&#x0630;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0649: "ى" ARABIC LETTER ALEF MAKSURA -->
     <Key
-        latin:keyLabel="&#x0649;" />
+        latin:keyLabel="&#x0649;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0629: "ة" ARABIC LETTER TEH MARBUTA -->
     <Key
-        latin:keyLabel="&#x0629;" />
+        latin:keyLabel="&#x0629;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0648: "و" ARABIC LETTER WAW -->
     <Key
-        latin:keyLabel="&#x0648;" />
+        latin:keyLabel="&#x0648;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0632: "ز" ARABIC LETTER ZAIN
          U+0698: "ژ" ARABIC LETTER JEH -->
     <Key
         latin:keyLabel="&#x0632;"
-        latin:moreKeys="&#x0698;" />
+        latin:moreKeys="&#x0698;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0638: "ظ" ARABIC LETTER ZAH -->
     <Key
-        latin:keyLabel="&#x0638;" />
+        latin:keyLabel="&#x0638;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062F: "د" ARABIC LETTER DAL -->
     <Key
-        latin:keyLabel="&#x062F;" />
+        latin:keyLabel="&#x062F;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml-sw600dp/rowkeys_farsi1.xml b/java/res/xml-sw600dp/rowkeys_farsi1.xml
index 53208f2..7b31240 100644
--- a/java/res/xml-sw600dp/rowkeys_farsi1.xml
+++ b/java/res/xml-sw600dp/rowkeys_farsi1.xml
@@ -23,25 +23,32 @@
 >
     <!-- U+0636: "ض" ARABIC LETTER DAD -->
     <Key
-        latin:keyLabel="&#x0636;" />
+        latin:keyLabel="&#x0636;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0635: "ص" ARABIC LETTER SAD -->
     <Key
-        latin:keyLabel="&#x0635;" />
+        latin:keyLabel="&#x0635;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062B: "ث" ARABIC LETTER THEH -->
     <Key
-        latin:keyLabel="&#x062B;" />
+        latin:keyLabel="&#x062B;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0642: "ق" ARABIC LETTER QAF -->
     <Key
-        latin:keyLabel="&#x0642;" />
+        latin:keyLabel="&#x0642;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0641: "ف" ARABIC LETTER FEH -->
     <Key
-        latin:keyLabel="&#x0641;" />
+        latin:keyLabel="&#x0641;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+063A: "غ" ARABIC LETTER GHAIN -->
     <Key
-        latin:keyLabel="&#x063A;" />
+        latin:keyLabel="&#x063A;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0639: "ع" ARABIC LETTER AIN -->
     <Key
-        latin:keyLabel="&#x0639;" />
+        latin:keyLabel="&#x0639;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0647: "ه" ARABIC LETTER HEH
          U+FEEB: "ﻫ" ARABIC LETTER HEH INITIAL FORM
          U+0647/U+200D: ARABIC LETTER HEH + ZERO WIDTH JOINER
@@ -49,17 +56,22 @@
          U+0629: "ة" ARABIC LETTER TEH MARBUTA -->
     <Key
         latin:keyLabel="&#x0647;"
-        latin:moreKeys="&#xFEEB;|&#x0647;&#x200D;,&#x0647;&#x0654;,&#x0629;,%" />
+        latin:moreKeys="&#xFEEB;|&#x0647;&#x200D;,&#x0647;&#x0654;,&#x0629;,%"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062E: "خ" ARABIC LETTER KHAH -->
     <Key
-        latin:keyLabel="&#x062E;" />
+        latin:keyLabel="&#x062E;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062D: "ح" ARABIC LETTER HAH -->
     <Key
-        latin:keyLabel="&#x062D;" />
+        latin:keyLabel="&#x062D;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062C: "ج" ARABIC LETTER JEEM -->
     <Key
-        latin:keyLabel="&#x062C;" />
+        latin:keyLabel="&#x062C;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0686: "چ" ARABIC LETTER TCHEH -->
     <Key
-        latin:keyLabel="&#x0686;" />
+        latin:keyLabel="&#x0686;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml-sw600dp/rowkeys_farsi2.xml b/java/res/xml-sw600dp/rowkeys_farsi2.xml
index 234f984..3b759b6 100644
--- a/java/res/xml-sw600dp/rowkeys_farsi2.xml
+++ b/java/res/xml-sw600dp/rowkeys_farsi2.xml
@@ -23,10 +23,12 @@
 >
     <!-- U+0634: "ش" ARABIC LETTER SHEEN -->
     <Key
-        latin:keyLabel="&#x0634;" />
+        latin:keyLabel="&#x0634;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0633: "س" ARABIC LETTER SEEN -->
     <Key
-        latin:keyLabel="&#x0633;" />
+        latin:keyLabel="&#x0633;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+06CC: "ی" ARABIC LETTER FARSI YEH
          U+0626: "ئ" ARABIC LETTER YEH WITH HAMZA ABOVE
          U+064A: "ي" ARABIC LETTER YEH
@@ -34,13 +36,16 @@
          U+0649: "ى" ARABIC LETTER ALEF MAKSURA -->
     <Key
         latin:keyLabel="&#x06CC;"
-        latin:moreKeys="&#x0626;,&#x064A;,&#xFBE8;|&#x0649;" />
+        latin:moreKeys="&#x0626;,&#x064A;,&#xFBE8;|&#x0649;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0628: "ب" ARABIC LETTER BEH -->
     <Key
-        latin:keyLabel="&#x0628;" />
+        latin:keyLabel="&#x0628;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0644: "ل" ARABIC LETTER LAM -->
     <Key
-        latin:keyLabel="&#x0644;" />
+        latin:keyLabel="&#x0644;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0627: "ا" ARABIC LETTER ALEF
          U+0621: "ء" ARABIC LETTER HAMZA
          U+0622: "آ" ARABIC LETTER ALEF WITH MADDA ABOVE
@@ -49,25 +54,31 @@
          U+0625: "إ" ARABIC LETTER ALEF WITH HAMZA BELOW -->
     <Key
         latin:keyLabel="&#x0627;"
-        latin:moreKeys="&#x0621;,&#x0622;,&#x0623;,&#x0671;,&#x0625;" />
+        latin:moreKeys="&#x0621;,&#x0622;,&#x0623;,&#x0671;,&#x0625;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062A: "ت" ARABIC LETTER TEH
          U+062B: "ﺙ" ARABIC LETTER THEH
          U+0629: "ة": ARABIC LETTER TEH MARBUTA -->
     <Key
         latin:keyLabel="&#x062A;"
-        latin:moreKeys="&#x062B;,&#x0629;" />
+        latin:moreKeys="&#x062B;,&#x0629;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0646: "ن" ARABIC LETTER NOON -->
     <Key
-        latin:keyLabel="&#x0646;" />
+        latin:keyLabel="&#x0646;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0645: "م" ARABIC LETTER MEEM -->
     <Key
-        latin:keyLabel="&#x0645;" />
+        latin:keyLabel="&#x0645;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+06A9: "ک" ARABIC LETTER KEHEH
          U+0643: "ك" ARABIC LETTER KAF -->
     <Key
         latin:keyLabel="&#x06A9;"
-        latin:moreKeys="&#x0643;" />
+        latin:moreKeys="&#x0643;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+06AF: "گ" ARABIC LETTER GAF -->
     <Key
-        latin:keyLabel="&#x06AF;" />
+        latin:keyLabel="&#x06AF;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml-sw600dp/rowkeys_farsi3.xml b/java/res/xml-sw600dp/rowkeys_farsi3.xml
index 998ba72..3597618 100644
--- a/java/res/xml-sw600dp/rowkeys_farsi3.xml
+++ b/java/res/xml-sw600dp/rowkeys_farsi3.xml
@@ -23,34 +23,44 @@
 >
     <!-- U+0638: "ظ" ARABIC LETTER ZAH -->
     <Key
-        latin:keyLabel="&#x0638;" />
+        latin:keyLabel="&#x0638;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0637: "ط" ARABIC LETTER TAH -->
     <Key
-        latin:keyLabel="&#x0637;" />
+        latin:keyLabel="&#x0637;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0698: "ژ" ARABIC LETTER JEH -->
     <Key
-        latin:keyLabel="&#x0698;" />
+        latin:keyLabel="&#x0698;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0632: "ز" ARABIC LETTER ZAIN -->
     <Key
-        latin:keyLabel="&#x0632;" />
+        latin:keyLabel="&#x0632;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0631: "ر" ARABIC LETTER REH -->
     <Key
-        latin:keyLabel="&#x0631;" />
+        latin:keyLabel="&#x0631;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0630: "ذ" ARABIC LETTER THAL -->
     <Key
-        latin:keyLabel="&#x0630;" />
+        latin:keyLabel="&#x0630;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062F: "د" ARABIC LETTER DAL -->
     <Key
-        latin:keyLabel="&#x062F;" />
+        latin:keyLabel="&#x062F;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+067E: "پ" ARABIC LETTER PEH -->
     <Key
-        latin:keyLabel="&#x067E;" />
+        latin:keyLabel="&#x067E;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0648: "و" ARABIC LETTER WAW
          U+0624: "ؤ" ARABIC LETTER WAW WITH HAMZA ABOVE -->
     <Key
         latin:keyLabel="&#x0648;"
-        latin:moreKeys="&#x0624;" />
+        latin:moreKeys="&#x0624;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0622: "آ" ARABIC LETTER ALEF WITH MADDA ABOVE -->
     <Key
-        latin:keyLabel="&#x0622;" />
+        latin:keyLabel="&#x0622;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml-sw600dp/rowkeys_symbols3.xml b/java/res/xml-sw600dp/rowkeys_symbols3.xml
index 4bfa0d7..30fba38 100644
--- a/java/res/xml-sw600dp/rowkeys_symbols3.xml
+++ b/java/res/xml-sw600dp/rowkeys_symbols3.xml
@@ -49,7 +49,7 @@
     <Key
         latin:keyLabel="." />
     <Key
-        latin:keyLabel="!text/keylabel_for_symbols_exclamation"
+        latin:keyLabel="!"
         latin:moreKeys="!text/more_keys_for_symbols_exclamation" />
     <Key
         latin:keyLabel="!text/keylabel_for_symbols_question"
diff --git a/java/res/xml-sw600dp/rowkeys_thai1.xml b/java/res/xml-sw600dp/rowkeys_thai1.xml
deleted file mode 100644
index 6aec7c2..0000000
--- a/java/res/xml-sw600dp/rowkeys_thai1.xml
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <switch>
-        <case
-            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
-        >
-            <!-- U+0E51: "๑" THAI DIGIT ONE -->
-            <Key
-                latin:keyLabel="&#x0E51;" />
-            <!-- U+0E52: "๒" THAI DIGIT TWO -->
-            <Key
-                latin:keyLabel="&#x0E52;" />
-            <!-- U+0E53: "๓" THAI DIGIT THREE -->
-            <Key
-                latin:keyLabel="&#x0E53;" />
-            <!-- U+0E54: "๔" THAI DIGIT FOUR -->
-            <Key
-                latin:keyLabel="&#x0E54;" />
-            <!-- U+0E39: " ู" THAI CHARACTER SARA UU -->
-            <Key
-                latin:keyLabel="&#x0E39;" />
-            <!-- U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT -->
-            <Key
-                latin:keyLabel="&#x0E3F;" />
-            <!-- U+0E55: "๕" THAI DIGIT FIVE -->
-            <Key
-                latin:keyLabel="&#x0E55;" />
-            <!-- U+0E56: "๖" THAI DIGIT SIX -->
-            <Key
-                latin:keyLabel="&#x0E56;" />
-            <!-- U+0E57: "๗" THAI DIGIT SEVEN -->
-            <Key
-                latin:keyLabel="&#x0E57;" />
-            <!-- U+0E58: "๘" THAI DIGIT EIGHT -->
-            <Key
-                latin:keyLabel="&#x0E58;" />
-            <!-- U+0E59: "๙" THAI DIGIT NINE -->
-            <Key
-                latin:keyLabel="&#x0E59;" />
-        </case>
-        <default>
-            <!-- U+0E45: "ๅ" THAI CHARACTER LAKKHANGYAO -->
-            <Key
-                latin:keyLabel="&#x0E45;" />
-            <Key
-                latin:keyLabel="/" />
-            <!-- U+0E20: "ภ" THAI CHARACTER PHO SAMPHAO -->
-            <Key
-                latin:keyLabel="&#x0E20;" />
-            <!-- U+0E16: "ถ" THAI CHARACTER THO THUNG -->
-            <Key
-                latin:keyLabel="&#x0E16;" />
-            <!-- U+0E38: " ุ" THAI CHARACTER SARA U -->
-            <Key
-                latin:keyLabel="&#x0E38;" />
-            <!-- U+0E36: " ึ" THAI CHARACTER SARA UE -->
-            <Key
-                latin:keyLabel="&#x0E36;" />
-            <!-- U+0E04: "ค" THAI CHARACTER KHO KHWAI -->
-            <Key
-                latin:keyLabel="&#x0E04;" />
-            <!-- U+0E15: "ต" THAI CHARACTER TO TAO -->
-            <Key
-                latin:keyLabel="&#x0E15;" />
-            <!-- U+0E08: "จ" THAI CHARACTER CHO CHAN -->
-            <Key
-                latin:keyLabel="&#x0E08;" />
-            <!-- U+0E02: "ข" THAI CHARACTER KHO KHAI -->
-            <Key
-                latin:keyLabel="&#x0E02;" />
-            <!-- U+0E0A: "ช" THAI CHARACTER CHO CHANG -->
-            <Key
-                latin:keyLabel="&#x0E0A;" />
-        </default>
-    </switch>
-</merge>
diff --git a/java/res/xml-sw600dp/rowkeys_thai2.xml b/java/res/xml-sw600dp/rowkeys_thai2.xml
deleted file mode 100644
index edb759a..0000000
--- a/java/res/xml-sw600dp/rowkeys_thai2.xml
+++ /dev/null
@@ -1,108 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <switch>
-        <case
-            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
-        >
-            <!-- U+0E50: "๐" THAI DIGIT ZERO -->
-            <Key
-                latin:keyLabel="&#x0E50;" />
-            <Key
-                latin:keyLabel="&quot;" />
-            <!-- U+0E0E: "ฎ" THAI CHARACTER DO CHADA -->
-            <Key
-                latin:keyLabel="&#x0E0E;" />
-            <!-- U+0E11: "ฑ" THAI CHARACTER THO NANGMONTHO -->
-            <Key
-                latin:keyLabel="&#x0E11;" />
-            <!-- U+0E18: "ธ" THAI CHARACTER THO THONG -->
-            <Key
-                latin:keyLabel="&#x0E18;" />
-            <!-- U+0E4D: " ํ" THAI CHARACTER THANTHAKHAT -->
-            <Key
-                latin:keyLabel="&#x0E4D;" />
-            <!-- U+0E4A: " ๊" THAI CHARACTER MAI TRI -->
-            <Key
-                latin:keyLabel="&#x0E4A;" />
-            <!-- U+0E13: "ณ" THAI CHARACTER NO NEN -->
-            <Key
-                latin:keyLabel="&#x0E13;" />
-            <!-- U+0E2F: "ฯ" THAI CHARACTER PAIYANNOI -->
-            <Key
-                latin:keyLabel="&#x0E2F;" />
-            <!-- U+0E0D: "ญ" THAI CHARACTER YO YING -->
-            <Key
-                latin:keyLabel="&#x0E0D;" />
-            <!-- U+0E10: "ฐ" THAI CHARACTER THO THAN -->
-            <Key
-                latin:keyLabel="&#x0E10;" />
-            <Key
-                latin:keyLabel="," />
-            <!-- U+0E05: "ฅ" THAI CHARACTER KHO KHON -->
-            <Key
-                latin:keyLabel="&#x0E05;" />
-        </case>
-        <default>
-            <!-- U+0E46: "ๆ" THAI CHARACTER MAIYAMOK -->
-            <Key
-                latin:keyLabel="&#x0E46;" />
-            <!-- U+0E44: "ไ" THAI CHARACTER SARA AI MAIMALAI -->
-            <Key
-                latin:keyLabel="&#x0E44;" />
-            <!-- U+0E33: "ำ" THAI CHARACTER SARA AM -->
-            <Key
-                latin:keyLabel="&#x0E33;" />
-            <!-- U+0E1E: "พ" THAI CHARACTER PHO PHAN -->
-            <Key
-                latin:keyLabel="&#x0E1E;" />
-            <!-- U+0E30: "ะ" THAI CHARACTER SARA A -->
-            <Key
-                latin:keyLabel="&#x0E30;" />
-            <!-- U+0E31: " ั" THAI CHARACTER MAI HAN-AKAT -->
-            <Key
-                latin:keyLabel="&#x0E31;" />
-            <!-- U+0E35: " ี" HAI CHARACTER SARA II -->
-            <Key
-                latin:keyLabel="&#x0E35;" />
-            <!-- U+0E23: "ร" THAI CHARACTER RO RUA -->
-            <Key
-                latin:keyLabel="&#x0E23;" />
-            <!-- U+0E19: "น" THAI CHARACTER NO NU -->
-            <Key
-                latin:keyLabel="&#x0E19;" />
-            <!-- U+0E22: "ย" THAI CHARACTER YO YAK -->
-            <Key
-                latin:keyLabel="&#x0E22;" />
-            <!-- U+0E1A: "บ" THAI CHARACTER BO BAIMAI -->
-            <Key
-                latin:keyLabel="&#x0E1A;" />
-            <!-- U+0E25: "ล" THAI CHARACTER LO LING -->
-            <Key
-                latin:keyLabel="&#x0E25;" />
-            <!-- U+0E03: "ฃ" THAI CHARACTER KHO KHUAT -->
-            <Key
-                latin:keyLabel="&#x0E03;" />
-        </default>
-    </switch>
-</merge>
diff --git a/java/res/xml-sw600dp/rowkeys_thai3.xml b/java/res/xml-sw600dp/rowkeys_thai3.xml
deleted file mode 100644
index 7507dde..0000000
--- a/java/res/xml-sw600dp/rowkeys_thai3.xml
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <switch>
-        <case
-            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
-        >
-            <!-- U+0E24: "ฤ" THAI CHARACTER RU -->
-            <Key
-                latin:keyLabel="&#x0E24;" />
-            <!-- U+0E06: "ฆ" THAI CHARACTER KHO RAKHANG -->
-            <Key
-                latin:keyLabel="&#x0E06;" />
-            <!-- U+0E0F: "ฏ" THAI CHARACTER TO PATAK -->
-            <Key
-                latin:keyLabel="&#x0E0F;" />
-            <!-- U+0E42: "โ" THAI CHARACTER SARA O -->
-            <Key
-                latin:keyLabel="&#x0E42;" />
-            <!-- U+0E0C: "ฌ" THAI CHARACTER CHO CHOE -->
-            <Key
-                latin:keyLabel="&#x0E0C;" />
-            <!-- U+0E47: " ็" THAI CHARACTER MAITAIKHU -->
-            <Key
-                latin:keyLabel="&#x0E47;" />
-            <!-- U+0E4B: " ๋" THAI CHARACTER MAI CHATTAWA -->
-            <Key
-                latin:keyLabel="&#x0E4B;" />
-            <!-- U+0E29: "ษ" THAI CHARACTER SO RUSI -->
-            <Key
-                latin:keyLabel="&#x0E29;" />
-            <!--  U+0E28: "ศ" THAI CHARACTER SO SALA -->
-            <Key
-                latin:keyLabel="&#x0E28;" />
-            <!-- U+0E0B: "ซ" THAI CHARACTER SO SO -->
-            <Key
-                latin:keyLabel="&#x0E0B;" />
-            <Key
-                latin:keyLabel="." />
-        </case>
-        <default>
-            <!-- U+0E1F: "ฟ" THAI CHARACTER FO FAN -->
-            <Key
-                latin:keyLabel="&#x0E1F;" />
-            <!-- U+0E2B: "ห" THAI CHARACTER HO HIP -->
-            <Key
-                latin:keyLabel="&#x0E2B;" />
-            <!-- U+0E01: "ก" THAI CHARACTER KO KAI -->
-            <Key
-                latin:keyLabel="&#x0E01;" />
-            <!-- U+0E14: "ด" THAI CHARACTER DO DEK -->
-            <Key
-                latin:keyLabel="&#x0E14;" />
-            <!-- U+0E40: "เ" THAI CHARACTER SARA E -->
-            <Key
-                latin:keyLabel="&#x0E40;" />
-            <!-- U+0E49: " ้" THAI CHARACTER MAI THO -->
-            <Key
-                latin:keyLabel="&#x0E49;" />
-            <!-- U+0E48: " ฺ" THAI CHARACTER MAI EK -->
-            <Key
-                latin:keyLabel="&#x0E48;" />
-            <!-- U+0E32: "า" THAI CHARACTER SARA AA -->
-            <Key
-                latin:keyLabel="&#x0E32;" />
-            <!-- U+0E2A: "ส" THAI CHARACTER SO SUA -->
-            <Key
-                latin:keyLabel="&#x0E2A;" />
-            <!-- U+0E27: "ว" THAI CHARACTER WO WAEN -->
-            <Key
-                latin:keyLabel="&#x0E27;" />
-            <!-- U+0E07: "ง" THAI CHARACTER NGO NGU -->
-            <Key
-                latin:keyLabel="&#x0E07;" />
-        </default>
-    </switch>
-</merge>
diff --git a/java/res/xml-sw600dp/rowkeys_thai4.xml b/java/res/xml-sw600dp/rowkeys_thai4.xml
deleted file mode 100644
index 64549bd..0000000
--- a/java/res/xml-sw600dp/rowkeys_thai4.xml
+++ /dev/null
@@ -1,89 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <switch>
-        <case
-            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
-        >
-            <Key
-                latin:keyLabel="(" />
-            <Key
-                latin:keyLabel=")" />
-            <!-- U+0E09: "ฉ" THAI CHARACTER CHO CHING -->
-            <Key
-                latin:keyLabel="&#x0E09;" />
-            <!-- U+0E2E: "ฮ" THAI CHARACTER HO NOKHUK -->
-            <Key
-                latin:keyLabel="&#x0E2E;" />
-            <!-- U+0E3A: " ฺ" THAI CHARACTER PHINTHU -->
-            <Key
-                latin:keyLabel="&#x0E3A;" />
-            <!-- U+0E4C: " ์" THAI CHARACTER THANTHAKHAT -->
-            <Key
-                latin:keyLabel="&#x0E4C;" />
-            <Key
-                latin:keyLabel="\?" />
-            <!-- U+0E12: "ฒ" THAI CHARACTER THO PHUTHAO -->
-            <Key
-                latin:keyLabel="&#x0E12;" />
-            <!-- U+0E2C: "ฬ" THAI CHARACTER LO CHULA -->
-            <Key
-                latin:keyLabel="&#x0E2C;" />
-            <!-- U+0E26: "ฦ" THAI CHARACTER LU -->
-            <Key
-                latin:keyLabel="&#x0E26;" />
-        </case>
-        <default>
-            <!-- U+0E1C: "ผ" THAI CHARACTER PHO PHUNG -->
-            <Key
-                latin:keyLabel="&#x0E1C;" />
-            <!-- U+0E1B: "ป" THAI CHARACTER PO PLA -->
-            <Key
-                latin:keyLabel="&#x0E1B;" />
-            <!-- U+0E41: "แ" THAI CHARACTER SARA AE -->
-            <Key
-                latin:keyLabel="&#x0E41;" />
-            <!-- U+0E2D: "อ" THAI CHARACTER O ANG -->
-            <Key
-                latin:keyLabel="&#x0E2D;" />
-            <!-- U+0E34: " ิ" THAI CHARACTER SARA I -->
-            <Key
-                latin:keyLabel="&#x0E34;" />
-            <!-- U+0E37: " ื" THAI CHARACTER SARA UEE -->
-            <Key
-                latin:keyLabel="&#x0E37;" />
-            <!-- U+0E17: "ท" THAI CHARACTER THO THAHAN -->
-            <Key
-                latin:keyLabel="&#x0E17;" />
-            <!-- U+0E21: "ม" THAI CHARACTER MO MA -->
-            <Key
-                latin:keyLabel="&#x0E21;" />
-            <!-- U+0E43: "ใ" THAI CHARACTER SARA AI MAIMUAN -->
-            <Key
-                latin:keyLabel="&#x0E43;" />
-            <!-- U+0E1D: "ฝ" THAI CHARACTER FO FA -->
-            <Key
-                latin:keyLabel="&#x0E1D;" />
-        </default>
-    </switch>
-</merge>
diff --git a/java/res/xml-sw600dp/rows_esperanto.xml b/java/res/xml-sw600dp/rows_esperanto.xml
deleted file mode 100644
index e0c62fe..0000000
--- a/java/res/xml-sw600dp/rows_esperanto.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/key_styles_common" />
-    <Row
-        latin:keyWidth="9.0%p"
-    >
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto1"
-            latin:keyLabelFlags="disableAdditionalMoreKeys|disableKeyHintLabel" />
-        <Key
-            latin:keyStyle="deleteKeyStyle"
-            latin:keyWidth="fillRight" />
-    </Row>
-    <Row
-        latin:keyWidth="9.0%p"
-    >
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto2" />
-        <Key
-            latin:keyStyle="enterKeyStyle"
-            latin:keyWidth="fillRight" />
-    </Row>
-    <Row
-        latin:keyWidth="9.0%p"
-    >
-        <Key
-            latin:keyStyle="shiftKeyStyle"
-            latin:keyWidth="10.0%p" />
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto3" />
-        <include
-            latin:keyboardLayout="@xml/keys_comma_period" />
-        <Key
-            latin:keyStyle="shiftKeyStyle"
-            latin:keyWidth="fillRight" />
-    </Row>
-    <include
-        latin:keyboardLayout="@xml/row_qwerty4" />
-</merge>
diff --git a/java/res/xml-sw600dp/rows_number_normal.xml b/java/res/xml-sw600dp/rows_number_normal.xml
index 48b3040..f692394 100644
--- a/java/res/xml-sw600dp/rows_number_normal.xml
+++ b/java/res/xml-sw600dp/rows_number_normal.xml
@@ -153,5 +153,8 @@
         <Key
             latin:keyLabel="#"
             latin:keyStyle="numKeyStyle" />
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw600dp/rows_thai.xml b/java/res/xml-sw600dp/rows_thai.xml
index c1fe55b..bc89640 100644
--- a/java/res/xml-sw600dp/rows_thai.xml
+++ b/java/res/xml-sw600dp/rows_thai.xml
@@ -27,8 +27,7 @@
         latin:keyWidth="7.5%p"
     >
         <include
-            latin:keyboardLayout="@xml/rowkeys_thai1"
-            latin:keyXPos="3.75%p" />
+            latin:keyboardLayout="@xml/rowkeys_thai1" />
         <Key
             latin:keyStyle="deleteKeyStyle"
             latin:keyWidth="fillRight" />
@@ -38,14 +37,16 @@
     >
         <include
             latin:keyboardLayout="@xml/rowkeys_thai2"
-            latin:keyXPos="0.719%p" />
+            latin:keyXPos="2.5%p" />
+        <include
+            latin:keyboardLayout="@xml/key_thai_kho_khuat" />
     </Row>
     <Row
         latin:keyWidth="7.5%p"
     >
         <include
             latin:keyboardLayout="@xml/rowkeys_thai3"
-            latin:keyXPos="3.75%p" />
+            latin:keyXPos="5.0%p" />
         <Key
             latin:keyStyle="enterKeyStyle"
             latin:keyWidth="fillRight" />
diff --git a/java/res/xml-sw768dp-land/kbd_thai.xml b/java/res/xml-sw768dp-land/kbd_thai.xml
deleted file mode 100644
index b2cdbc3..0000000
--- a/java/res/xml-sw768dp-land/kbd_thai.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
-*/
--->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:rowHeight="20%p"
-    latin:verticalGap="2.65%p"
-    latin:touchPositionCorrectionData="@null"
->
-    <include
-        latin:keyboardLayout="@xml/rows_thai" />
-</Keyboard>
diff --git a/java/res/xml-sw768dp-land/kbd_thai_symbols.xml b/java/res/xml-sw768dp-land/kbd_thai_symbols.xml
deleted file mode 100644
index 1531458..0000000
--- a/java/res/xml-sw768dp-land/kbd_thai_symbols.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
-*/
--->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:rowHeight="20%p"
-    latin:verticalGap="2.65%p"
-    latin:touchPositionCorrectionData="@null"
->
-    <include
-        latin:keyboardLayout="@xml/rows_thai_symbols" />
-</Keyboard>
diff --git a/java/res/xml-sw768dp-land/kbd_thai_symbols_shift.xml b/java/res/xml-sw768dp-land/kbd_thai_symbols_shift.xml
deleted file mode 100644
index fa30f24..0000000
--- a/java/res/xml-sw768dp-land/kbd_thai_symbols_shift.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
-*/
--->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:rowHeight="20%p"
-    latin:verticalGap="2.65%p"
-    latin:touchPositionCorrectionData="@null"
->
-    <include
-        latin:keyboardLayout="@xml/rows_thai_symbols_shift" />
-</Keyboard>
diff --git a/java/res/xml-sw768dp/kbd_thai.xml b/java/res/xml-sw768dp/kbd_thai.xml
deleted file mode 100644
index 593ccbd..0000000
--- a/java/res/xml-sw768dp/kbd_thai.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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.
-*/
--->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:rowHeight="20%p"
-    latin:verticalGap="2.95%p"
-    latin:touchPositionCorrectionData="@null"
->
-    <include
-        latin:keyboardLayout="@xml/rows_thai" />
-</Keyboard>
diff --git a/java/res/xml-sw768dp/kbd_thai_symbols.xml b/java/res/xml-sw768dp/kbd_thai_symbols.xml
index e2e5f5d..0cd9a61 100644
--- a/java/res/xml-sw768dp/kbd_thai_symbols.xml
+++ b/java/res/xml-sw768dp/kbd_thai_symbols.xml
@@ -21,7 +21,9 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:rowHeight="20%p"
-    latin:verticalGap="2.95%p"
+    latin:verticalGap="@fraction/key_bottom_gap_5row"
+    latin:keyLetterSize="@fraction/key_letter_ratio_5row"
+    latin:keyShiftedLetterHintRatio="@fraction/key_uppercase_letter_ratio_5row"
     latin:touchPositionCorrectionData="@null"
 >
     <include
diff --git a/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml b/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml
index a1358d4..a68fec4 100644
--- a/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml
+++ b/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml
@@ -21,7 +21,9 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:rowHeight="20%p"
-    latin:verticalGap="2.95%p"
+    latin:verticalGap="@fraction/key_bottom_gap_5row"
+    latin:keyLetterSize="@fraction/key_letter_ratio_5row"
+    latin:keyShiftedLetterHintRatio="@fraction/key_uppercase_letter_ratio_5row"
     latin:touchPositionCorrectionData="@null"
 >
     <include
diff --git a/java/res/xml-sw768dp/key_space.xml b/java/res/xml-sw768dp/key_space.xml
index 8968f08..58e71d8 100644
--- a/java/res/xml-sw768dp/key_space.xml
+++ b/java/res/xml-sw768dp/key_space.xml
@@ -24,15 +24,36 @@
     <switch>
         <case
             latin:languageCode="fa"
+            latin:languageSwitchKeyEnabled="true"
+        >
+            <Key
+                latin:keyStyle="languageSwitchKeyStyle" />
+            <Key
+                latin:keyStyle="spaceKeyStyle"
+                latin:keyWidth="24.141%p" />
+            <Key
+                latin:keyStyle="zwnjKeyStyle" />
+        </case>
+        <case
+            latin:languageCode="fa"
+            latin:languageSwitchKeyEnabled="false"
         >
             <Key
                 latin:keyStyle="spaceKeyStyle"
                 latin:keyWidth="32.188%p" />
-            <!-- U+200C: "" ZERO WIDTH NON-JOINER
-                 U+200D: "" ZERO WIDTH JOINER -->
             <Key
                 latin:keyStyle="zwnjKeyStyle" />
         </case>
+        <case
+            latin:languageSwitchKeyEnabled="true"
+        >
+            <Key
+                latin:keyStyle="languageSwitchKeyStyle" />
+            <Key
+                latin:keyStyle="spaceKeyStyle"
+                latin:keyWidth="32.188%p" />
+        </case>
+         <!-- languageSwitchKeyEnabled="false" -->
         <default>
             <Key
                 latin:keyStyle="spaceKeyStyle"
diff --git a/java/res/xml-sw768dp/key_styles_common.xml b/java/res/xml-sw768dp/key_styles_common.xml
index 40082ac..537e768 100644
--- a/java/res/xml-sw768dp/key_styles_common.xml
+++ b/java/res/xml-sw768dp/key_styles_common.xml
@@ -76,7 +76,7 @@
     <key-style
         latin:styleName="spaceKeyStyle"
         latin:code="!code/key_space"
-        latin:keyActionFlags="noKeyPreview" />
+        latin:keyActionFlags="noKeyPreview|enableLongPress" />
     <!-- U+200C: ZERO WIDTH NON-JOINER
          U+200D: ZERO WIDTH JOINER -->
     <key-style
@@ -100,6 +100,12 @@
         latin:keyActionFlags="noKeyPreview"
         latin:backgroundType="functional" />
     <key-style
+        latin:styleName="languageSwitchKeyStyle"
+        latin:code="!code/key_language_switch"
+        latin:keyIcon="!icon/language_switch_key"
+        latin:keyActionFlags="noKeyPreview|altCodeWhileTyping|enableLongPress"
+        latin:altCode="!code/key_space" />
+    <key-style
         latin:styleName="settingsKeyStyle"
         latin:code="!code/key_settings"
         latin:keyIcon="!icon/settings_key"
@@ -117,6 +123,17 @@
                 latin:keyLabelFlags="fontNormal|preserveCase"
                 latin:backgroundType="functional" />
         </case>
+        <case
+            latin:keyboardLayoutSetElement="alphabet|alphabetAutomaticShifted|alphabetShiftLocked"
+            latin:navigateNext="true"
+        >
+            <key-style
+                latin:styleName="tabKeyStyle"
+                latin:code="!code/key_action_next"
+                latin:keyLabel="!text/label_tab_key"
+                latin:keyLabelFlags="fontNormal|preserveCase"
+                latin:backgroundType="functional" />
+        </case>
         <default>
             <key-style
                 latin:styleName="tabKeyStyle"
diff --git a/java/res/xml-sw768dp/row_dvorak4.xml b/java/res/xml-sw768dp/row_dvorak4.xml
index 0827815..8f9230d 100644
--- a/java/res/xml-sw768dp/row_dvorak4.xml
+++ b/java/res/xml-sw768dp/row_dvorak4.xml
@@ -25,8 +25,10 @@
         latin:keyWidth="8.047%p"
         latin:backgroundType="functional"
     >
+        <!-- Note: This Spacer prevents the below key from being marked as a left edge key. -->
+        <Spacer
+            latin:keyWidth="5.782%p" />
         <include
-            latin:keyXPos="5.782%p"
             latin:keyboardLayout="@xml/key_settings" />
         <include
             latin:keyboardLayout="@xml/key_shortcut" />
@@ -42,5 +44,8 @@
             latin:keyboardLayout="@xml/key_dash" />
         <include
             latin:keyboardLayout="@xml/key_f2" />
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw768dp/row_hebrew4.xml b/java/res/xml-sw768dp/row_hebrew4.xml
index 180c564..ae14f02 100644
--- a/java/res/xml-sw768dp/row_hebrew4.xml
+++ b/java/res/xml-sw768dp/row_hebrew4.xml
@@ -25,8 +25,10 @@
         latin:keyWidth="8.047%p"
         latin:backgroundType="functional"
     >
+        <!-- Note: This Spacer prevents the below key from being marked as a left edge key. -->
+        <Spacer
+            latin:keyWidth="5.782%p" />
         <include
-            latin:keyXPos="5.782%p"
             latin:keyboardLayout="@xml/key_settings" />
         <include
             latin:keyboardLayout="@xml/key_shortcut" />
@@ -40,5 +42,8 @@
             latin:keyboardLayout="@xml/keys_comma_period" />
         <include
             latin:keyboardLayout="@xml/key_f2" />
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw768dp/row_qwerty4.xml b/java/res/xml-sw768dp/row_qwerty4.xml
index 92411f5..f1f4214 100644
--- a/java/res/xml-sw768dp/row_qwerty4.xml
+++ b/java/res/xml-sw768dp/row_qwerty4.xml
@@ -25,8 +25,10 @@
         latin:keyWidth="8.047%p"
         latin:backgroundType="functional"
     >
+        <!-- Note: This Spacer prevents the below key from being marked as a left edge key. -->
+        <Spacer
+            latin:keyWidth="5.782%p" />
         <include
-            latin:keyXPos="5.782%p"
             latin:keyboardLayout="@xml/key_settings" />
         <include
             latin:keyboardLayout="@xml/key_shortcut" />
@@ -42,5 +44,8 @@
             latin:keyboardLayout="@xml/key_dash" />
         <include
             latin:keyboardLayout="@xml/key_f2" />
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw768dp/row_symbols4.xml b/java/res/xml-sw768dp/row_symbols4.xml
index 4e1c119..b801a12 100644
--- a/java/res/xml-sw768dp/row_symbols4.xml
+++ b/java/res/xml-sw768dp/row_symbols4.xml
@@ -25,8 +25,10 @@
         latin:keyWidth="8.047%p"
         latin:backgroundType="functional"
     >
+        <!-- Note: This Spacer prevents the below key from being marked as a left edge key. -->
+        <Spacer
+            latin:keyWidth="13.829%p" />
         <Key
-            latin:keyXPos="13.829%p"
             latin:keyLabel="/" />
         <include
             latin:keyboardLayout="@xml/key_f1" />
@@ -39,6 +41,8 @@
             latin:moreKeys="!text/more_keys_for_tablet_double_quote" />
         <Key
             latin:keyLabel="_" />
-        <!-- Here is empty space. -->
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw768dp/row_symbols_shift4.xml b/java/res/xml-sw768dp/row_symbols_shift4.xml
index 561351c..f71864b 100644
--- a/java/res/xml-sw768dp/row_symbols_shift4.xml
+++ b/java/res/xml-sw768dp/row_symbols_shift4.xml
@@ -25,11 +25,14 @@
         latin:keyWidth="8.047%p"
         latin:backgroundType="functional"
     >
-        <!-- Here is empty space. -->
+        <!-- Note: This Spacer prevents the below key from being marked as a left edge key. -->
+        <Spacer
+            latin:keyWidth="29.923%p" />
         <include
-            latin:keyXPos="29.923%p"
             latin:keyboardLayout="@xml/key_space"
             latin:backgroundType="normal" />
-        <!-- Here is empty space. -->
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw768dp/rowkeys_thai_digits.xml b/java/res/xml-sw768dp/rowkeys_thai_digits.xml
index 5122830..55196eb 100644
--- a/java/res/xml-sw768dp/rowkeys_thai_digits.xml
+++ b/java/res/xml-sw768dp/rowkeys_thai_digits.xml
@@ -23,32 +23,42 @@
 >
     <!-- U+0E51: "๑" THAI DIGIT ONE -->
     <Key
-        latin:keyLabel="&#x0E51;" />
+        latin:keyLabel="&#x0E51;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E52: "๒" THAI DIGIT TWO -->
     <Key
-        latin:keyLabel="&#x0E52;" />
+        latin:keyLabel="&#x0E52;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E53: "๓" THAI DIGIT THREE -->
     <Key
-        latin:keyLabel="&#x0E53;" />
+        latin:keyLabel="&#x0E53;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E54: "๔" THAI DIGIT FOUR -->
     <Key
-        latin:keyLabel="&#x0E54;" />
+        latin:keyLabel="&#x0E54;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E55: "๕" THAI DIGIT FIVE -->
     <Key
-        latin:keyLabel="&#x0E55;" />
+        latin:keyLabel="&#x0E55;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E56: "๖" THAI DIGIT SIX -->
     <Key
-        latin:keyLabel="&#x0E56;" />
+        latin:keyLabel="&#x0E56;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E57: "๗" THAI DIGIT SEVEN -->
     <Key
-        latin:keyLabel="&#x0E57;" />
+        latin:keyLabel="&#x0E57;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E58: "๘" THAI DIGIT EIGHT -->
     <Key
-        latin:keyLabel="&#x0E58;" />
+        latin:keyLabel="&#x0E58;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E59: "๙" THAI DIGIT NINE -->
     <Key
-        latin:keyLabel="&#x0E59;" />
+        latin:keyLabel="&#x0E59;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0E50: "๐" THAI DIGIT ZERO -->
     <Key
-        latin:keyLabel="&#x0E50;" />
+        latin:keyLabel="&#x0E50;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml-sw768dp/rows_east_slavic.xml b/java/res/xml-sw768dp/rows_east_slavic.xml
index 0316c76..a4287f1 100644
--- a/java/res/xml-sw768dp/rows_east_slavic.xml
+++ b/java/res/xml-sw768dp/rows_east_slavic.xml
@@ -33,9 +33,8 @@
         <include
             latin:keyboardLayout="@xml/rowkeys_east_slavic1"
             latin:keyLabelFlags="disableAdditionalMoreKeys|disableKeyHintLabel" />
-        <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
         <Key
-            latin:keyLabel="&#x044A;" />
+            latin:keyLabel="!text/keylabel_for_east_slavic_row1_12" />
         <Key
             latin:keyStyle="deleteKeyStyle"
             latin:keyWidth="fillRight" />
diff --git a/java/res/xml-sw768dp/rows_esperanto.xml b/java/res/xml-sw768dp/rows_esperanto.xml
deleted file mode 100644
index 0b3bb1f..0000000
--- a/java/res/xml-sw768dp/rows_esperanto.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/key_styles_common" />
-    <Row
-        latin:keyWidth="8.282%p"
-    >
-        <Key
-            latin:keyStyle="tabKeyStyle"
-            latin:keyLabelFlags="alignLeft"
-            latin:keyWidth="7.969%p" />
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto1"
-            latin:keyLabelFlags="disableAdditionalMoreKeys|disableKeyHintLabel" />
-        <Key
-            latin:keyStyle="deleteKeyStyle"
-            latin:keyWidth="fillRight"/>
-    </Row>
-    <Row
-        latin:keyWidth="8.125%p"
-    >
-        <Key
-            latin:keyStyle="toSymbolKeyStyle"
-            latin:keyLabelFlags="alignLeft"
-            latin:keyWidth="10.167%"/>
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto2" />
-        <Key
-            latin:keyStyle="enterKeyStyle"
-            latin:keyWidth="fillRight" />
-    </Row>
-    <Row
-        latin:keyWidth="8.047%p"
-    >
-        <Key
-            latin:keyStyle="shiftKeyStyle"
-            latin:keyWidth="13.829%p"/>
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto3" />
-        <include
-            latin:keyboardLayout="@xml/keys_comma_period" />
-        <Key
-            latin:keyStyle="shiftKeyStyle"
-            latin:keyWidth="fillRight" />
-    </Row>
-    <include
-        latin:keyboardLayout="@xml/row_qwerty4" />
-</merge>
diff --git a/java/res/xml-sw768dp/rows_number_normal.xml b/java/res/xml-sw768dp/rows_number_normal.xml
index 84910a8..d4d7c72 100644
--- a/java/res/xml-sw768dp/rows_number_normal.xml
+++ b/java/res/xml-sw768dp/rows_number_normal.xml
@@ -168,5 +168,8 @@
         <Key
             latin:keyLabel="#"
             latin:keyStyle="numKeyStyle" />
+        <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
+        <Spacer
+            latin:keyWidth="fillRight" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw768dp/rows_thai.xml b/java/res/xml-sw768dp/rows_thai.xml
index 7721bc5..5f9b383 100644
--- a/java/res/xml-sw768dp/rows_thai.xml
+++ b/java/res/xml-sw768dp/rows_thai.xml
@@ -28,7 +28,7 @@
     >
         <include
             latin:keyboardLayout="@xml/rowkeys_thai1"
-            latin:keyXPos="11.508%p" />
+            latin:keyXPos="3.799%p" />
         <Key
             latin:keyStyle="deleteKeyStyle"
             latin:keyWidth="fillRight"/>
@@ -42,9 +42,11 @@
             latin:keyWidth="7.969%p" />
         <include
             latin:keyboardLayout="@xml/rowkeys_thai2" />
+        <include
+            latin:keyboardLayout="@xml/key_thai_kho_khuat" />
     </Row>
     <Row
-        latin:keyWidth="7.125%p"
+        latin:keyWidth="7.079%p"
     >
         <Key
             latin:keyStyle="toSymbolKeyStyle"
diff --git a/java/res/xml/kbd_more_keys_keyboard_template.xml b/java/res/xml/kbd_more_keys_keyboard_template.xml
index 8e977c5..537973d 100644
--- a/java/res/xml/kbd_more_keys_keyboard_template.xml
+++ b/java/res/xml/kbd_more_keys_keyboard_template.xml
@@ -1,19 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-/* 
+/*
 **
 ** Copyright 2008, The Android Open Source Project
 **
-** Licensed under the Apache License, Version 2.0 (the "License"); 
-** you may not use this file except in compliance with the License. 
-** You may obtain a copy of the License at 
+** 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 
+**     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 
+** 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.
 */
 -->
diff --git a/java/res/xml/kbd_pcqwerty.xml b/java/res/xml/kbd_pcqwerty.xml
index cebca4f..777c71a 100644
--- a/java/res/xml/kbd_pcqwerty.xml
+++ b/java/res/xml/kbd_pcqwerty.xml
@@ -21,7 +21,9 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:rowHeight="20%p"
-    latin:verticalGap="3.20%p"
+    latin:verticalGap="@fraction/key_bottom_gap_5row"
+    latin:keyLetterSize="@fraction/key_letter_ratio_5row"
+    latin:keyShiftedLetterHintRatio="@fraction/key_uppercase_letter_ratio_5row"
     latin:touchPositionCorrectionData="@null"
 >
     <include
diff --git a/java/res/xml/kbd_pcqwerty_symbols.xml b/java/res/xml/kbd_pcqwerty_symbols.xml
index fd64e5b..a2297f7 100644
--- a/java/res/xml/kbd_pcqwerty_symbols.xml
+++ b/java/res/xml/kbd_pcqwerty_symbols.xml
@@ -21,7 +21,9 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:rowHeight="20%p"
-    latin:verticalGap="3.20%p"
+    latin:verticalGap="@fraction/key_bottom_gap_5row"
+    latin:keyLetterSize="@fraction/key_letter_ratio_5row"
+    latin:keyShiftedLetterHintRatio="@fraction/key_uppercase_letter_ratio_5row"
     latin:touchPositionCorrectionData="@null"
 >
     <include
diff --git a/java/res/xml/kbd_thai.xml b/java/res/xml/kbd_thai.xml
index 058ca16..b4a4a0b 100644
--- a/java/res/xml/kbd_thai.xml
+++ b/java/res/xml/kbd_thai.xml
@@ -20,6 +20,11 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:rowHeight="20%p"
+    latin:verticalGap="@fraction/key_bottom_gap_5row"
+    latin:keyLetterSize="@fraction/key_letter_ratio_5row"
+    latin:keyShiftedLetterHintRatio="@fraction/key_uppercase_letter_ratio_5row"
+    latin:touchPositionCorrectionData="@null"
 >
     <include
         latin:keyboardLayout="@xml/rows_thai" />
diff --git a/java/res/xml/key_styles_common.xml b/java/res/xml/key_styles_common.xml
index 622da21..162119d 100644
--- a/java/res/xml/key_styles_common.xml
+++ b/java/res/xml/key_styles_common.xml
@@ -22,23 +22,8 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <!-- Base key style for the key which may have settings or tab key as popup key. -->
-    <switch>
-        <case
-            latin:clobberSettingsKey="true"
-        >
-            <key-style
-                latin:styleName="f1MoreKeysStyle"
-                latin:backgroundType="functional" />
-        </case>
-        <!-- clobberSettingsKey="false" -->
-        <default>
-            <key-style
-                latin:styleName="f1MoreKeysStyle"
-                latin:keyLabelFlags="hasPopupHint"
-                latin:moreKeys="!text/settings_as_more_key"
-                latin:backgroundType="functional" />
-        </default>
-    </switch>
+    <include
+        latin:keyboardLayout="@xml/key_styles_f1" />
     <!-- Functional key styles -->
     <switch>
         <case
diff --git a/java/res/xml/key_styles_f1.xml b/java/res/xml/key_styles_f1.xml
new file mode 100644
index 0000000..8dfc3cb
--- /dev/null
+++ b/java/res/xml/key_styles_f1.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <!-- Base key style for the key which may have settings or tab key as popup key. -->
+    <!-- Kept as a separate file for cleaner overriding by an overlay.  -->
+    <switch>
+        <case
+            latin:clobberSettingsKey="true"
+        >
+            <key-style
+                latin:styleName="f1MoreKeysStyle"
+                latin:backgroundType="functional" />
+        </case>
+        <!-- clobberSettingsKey="false" -->
+        <default>
+            <key-style
+                latin:styleName="f1MoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:moreKeys="!text/settings_as_more_key"
+                latin:backgroundType="functional" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/key_thai_kho_khuat.xml b/java/res/xml/key_thai_kho_khuat.xml
new file mode 100644
index 0000000..0ffd0f9
--- /dev/null
+++ b/java/res/xml/key_thai_kho_khuat.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <switch>
+        <case
+            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
+        >
+            <!-- U+0E05: "ฅ" THAI CHARACTER KHO KHON -->
+            <Key
+                latin:keyLabel="&#x0E05;"
+                latin:keyLabelFlags="fontNormal" />
+        </case>
+        <default>
+            <!-- U+0E03: "ฃ" THAI CHARACTER KHO KHUAT -->
+            <Key
+                latin:keyLabel="&#x0E03;"
+                latin:keyLabelFlags="fontNormal" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/keyboard_layout_set_esperanto.xml b/java/res/xml/keyboard_layout_set_esperanto.xml
deleted file mode 100644
index 94a386d..0000000
--- a/java/res/xml/keyboard_layout_set_esperanto.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?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.
-*/
--->
-
-<KeyboardLayoutSet
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin">
-    <Element
-        latin:elementName="alphabet"
-        latin:elementKeyboard="@xml/kbd_esperanto"
-        latin:enableProximityCharsCorrection="true" />
-    <Element
-        latin:elementName="symbols"
-        latin:elementKeyboard="@xml/kbd_symbols" />
-    <Element
-        latin:elementName="symbolsShifted"
-        latin:elementKeyboard="@xml/kbd_symbols_shift" />
-    <Element
-        latin:elementName="phone"
-        latin:elementKeyboard="@xml/kbd_phone" />
-    <Element
-        latin:elementName="phoneSymbols"
-        latin:elementKeyboard="@xml/kbd_phone_symbols" />
-    <Element
-        latin:elementName="number"
-        latin:elementKeyboard="@xml/kbd_number" />
-</KeyboardLayoutSet>
diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml
index 8d4b85d..83eafd0 100644
--- a/java/res/xml/method.xml
+++ b/java/res/xml/method.xml
@@ -22,17 +22,19 @@
 
 <!-- Supported subtypes
     keyboard_locale: script_name/keyboard_layout_set[:keyboard_locale]
+    af: Afrikaans/qwerty
     ar: Arabic/arabic
     be: Belarusian/east_slavic
     bg: Bulgarian/bulgarian
     bg: Bulgarian/bulgarian_bds
+    ca: Catalan/spanish
     cs: Czech/qwertz
     da: Danish/nordic
     de: German/qwertz
     el: Greek/greek
     en_US: English United States/qwerty
     en_GB: English Great Britain/qwerty
-    eo: Esperanto/esperanto
+    eo: Esperanto/spanish
     es: Spanish/spanish
     et: Estonian/nordic
     fa: Persian/arabic
@@ -42,14 +44,16 @@
     hi: Hindi/hindi
     hr: Croatian/qwertz
     hu: Hungarian/qwertz
+    in: Indonesian/qwerty    # "id" is official language code of Indonesian.
     is: Icelandic/qwerty
     it: Italian/qwerty
-    iw: Hebrew/hebrew
+    iw: Hebrew/hebrew        # "he" is official language code of Hebrew.
     ka: Georgian/georgian
     ky: Kyrgyz/east_slavic
     lt: Lithuanian/qwerty
     lv: Latvian/qwerty
     mk: Macedonian/south_slavic
+    ms: Malay/qwerty
     nb: Norwegian Bokmål/nordic
     nl: Dutch/qwerty
     nl_BE: Dutch Belgium/azerty
@@ -61,14 +65,20 @@
     sk: Slovak/qwerty
     sl: Slovenian/qwerty
     sr: Serbian/south_slavic
+    (sr-Latn: Serbian/qwerty) # not yet implemented.
     sv: Swedish/nordic
+    sw: Swahili/qwerty
     th: Thai/thai
+    tl: Tagalog/spanish
     tr: Turkish/qwerty
     uk: Ukrainian/east_slavic
     vi: Vietnamese/qwerty
+    zu: Zulu/qwerty
     zz: QWERTY/qwerty
     -->
 <!-- TODO: use <lang>_keyboard icon instead of a common keyboard icon. -->
+<!-- Note: SupportTouchPositionCorrection extra value is obsolete and maintained for backward
+     compatibility. -->
 <!-- If IME doesn't have an applicable subtype, the first subtype will be used as a default
      subtype.-->
 <input-method xmlns:android="http://schemas.android.com/apk/res/android"
@@ -76,132 +86,176 @@
         android:isDefault="@bool/im_is_default">
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_en_US"
+            android:subtypeId="-921088104"
             android:imeSubtypeLocale="en_US"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="TrySuppressingImeSwitcher,AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_en_GB"
+            android:subtypeId="-1337596075"
             android:imeSubtypeLocale="en_GB"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="TrySuppressingImeSwitcher,AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1872175968"
+            android:imeSubtypeLocale="af"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
+            android:subtypeId="1494081088"
             android:imeSubtypeLocale="ar"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="499361881"
             android:imeSubtypeLocale="be"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=east_slavic"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="195674344"
             android:imeSubtypeLocale="bg"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=bulgarian"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_bulgarian_bds"
+            android:subtypeId="1599191706"
             android:imeSubtypeLocale="bg"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=bulgarian_bds"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-756735787"
+            android:imeSubtypeLocale="ca"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=spanish,AsciiCapable"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
+            android:subtypeId="758984400"
             android:imeSubtypeLocale="cs"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="770990173"
             android:imeSubtypeLocale="da"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="774684257"
             android:imeSubtypeLocale="de"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="242746067"
             android:imeSubtypeLocale="el"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=greek"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1083200842"
             android:imeSubtypeLocale="eo"
             android:imeSubtypeMode="keyboard"
-            android:imeSubtypeExtraValue="KeyboardLayoutSet=esperanto,AsciiCapable"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=spanish"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="816242702"
             android:imeSubtypeLocale="es"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-332580523"
             android:imeSubtypeLocale="et"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=nordic,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-1100561836"
             android:imeSubtypeLocale="fa"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=farsi"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="835636643"
             android:imeSubtypeLocale="fi"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="843948332"
             android:imeSubtypeLocale="fr"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-354699631"
             android:imeSubtypeLocale="fr_CA"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="963984255"
             android:imeSubtypeLocale="hi"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=hindi"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="901206634"
             android:imeSubtypeLocale="hr"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="903977197"
             android:imeSubtypeLocale="hu"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
+    <!-- Java uses the deprecated "in" code instead of the standard "id" code for Indonesian. -->
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="2108597344"
+            android:imeSubtypeLocale="in"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
+            android:subtypeId="2113214949"
             android:imeSubtypeLocale="is"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="931682827"
             android:imeSubtypeLocale="it"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
@@ -209,48 +263,63 @@
     <!-- Java uses the deprecated "iw" code instead of the standard "he" code for Hebrew. -->
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1727731901"
             android:imeSubtypeLocale="iw"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1846648426"
             android:imeSubtypeLocale="ka"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=georgian"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="775494660"
             android:imeSubtypeLocale="ky"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=east_slavic"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-2094941373"
             android:imeSubtypeLocale="lt"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-2093094331"
             android:imeSubtypeLocale="lv"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-1353667716"
             android:imeSubtypeLocale="mk"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=south_slavic"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-2067235743"
+            android:imeSubtypeLocale="ms"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
+            android:subtypeId="1058205204"
             android:imeSubtypeLocale="nb"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1067440414"
             android:imeSubtypeLocale="nl"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
@@ -263,84 +332,135 @@
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1124698716"
             android:imeSubtypeLocale="pl"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-889195354"
             android:imeSubtypeLocale="pt_BR"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-486540198"
             android:imeSubtypeLocale="pt_PT"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-1927784072"
             android:imeSubtypeLocale="ro"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1983547218"
             android:imeSubtypeLocale="ru"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-1902849005"
             android:imeSubtypeLocale="sk"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-1901925484"
             android:imeSubtypeLocale="sl"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="2009405806"
+            android:imeSubtypeLocale="sr"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="SupportTouchPositionCorrection"
+    />
+    <!-- TODO: Uncomment once we can handle IETF language tag with script name specified.
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_serbian_cyrillic"
+            android:subtypeId="XXXXXX"
             android:imeSubtypeLocale="sr"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_serbian_latin"
+            android:subtypeId="XXXXXX"
+            android:imeSubtypeLocale="sr-Latn"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
+    />
+    -->
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1219821379"
             android:imeSubtypeLocale="sv"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-1891766753"
+            android:imeSubtypeLocale="sw"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
+            android:subtypeId="529847764"
             android:imeSubtypeLocale="th"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=thai"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-259881489"
+            android:imeSubtypeLocale="tl"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=spanish,AsciiCapable"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
+            android:subtypeId="1244756446"
             android:imeSubtypeLocale="tr"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="1048856876"
             android:imeSubtypeLocale="uk"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=east_slavic"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="-1818808594"
             android:imeSubtypeLocale="vi"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
+            android:subtypeId="-1693209738"
+            android:imeSubtypeLocale="zu"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_no_language_qwerty"
+            android:subtypeId="-1573262419"
             android:imeSubtypeLocale="zz"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="KeyboardLayoutSet=qwerty,AsciiCapable,EnabledWhenDefaultIsNotAsciiCapable"
diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml
index 1379819..478dc0f 100644
--- a/java/res/xml/prefs.xml
+++ b/java/res/xml/prefs.xml
@@ -81,19 +81,33 @@
         android:title="@string/misc_category"
         android:key="misc_settings">
         <CheckBoxPreference
-            android:key="usability_study_mode"
-            android:title="@string/prefs_usability_study_mode"
+            android:key="next_word_prediction"
+            android:title="@string/bigram_prediction"
+            android:summary="@string/bigram_prediction_summary"
             android:persistent="true"
-            android:defaultValue="false" />
+            android:defaultValue="true" />
+        <CheckBoxPreference
+            android:key="gesture_input"
+            android:title="@string/gesture_input"
+            android:summary="@string/gesture_input_summary"
+            android:persistent="true"
+            android:defaultValue="true" />
         <PreferenceScreen
             android:key="pref_advanced_settings"
             android:title="@string/advanced_settings"
             android:summary="@string/advanced_settings_summary">
             <CheckBoxPreference
-                android:key="pref_suppress_language_switch_key"
-                android:title="@string/suppress_language_switch_key"
+                android:key="pref_key_use_contacts_dict"
+                android:title="@string/use_contacts_dict"
+                android:summary="@string/use_contacts_dict_summary"
                 android:persistent="true"
-                android:defaultValue="false" />
+                android:defaultValue="true" />
+            <CheckBoxPreference
+                android:key="pref_show_language_switch_key"
+                android:title="@string/show_language_switch_key"
+                android:summary="@string/show_language_switch_key_summary"
+                android:persistent="true"
+                android:defaultValue="true" />
             <CheckBoxPreference
                 android:key="pref_include_other_imes_in_language_switch_list"
                 android:title="@string/include_other_imes_in_language_switch_list"
@@ -108,36 +122,28 @@
             <ListPreference
                 android:key="pref_key_preview_popup_dismiss_delay"
                 android:title="@string/key_preview_popup_dismiss_delay" />
-            <CheckBoxPreference
-                android:key="pref_key_use_contacts_dict"
-                android:title="@string/use_contacts_dict"
-                android:summary="@string/use_contacts_dict_summary"
-                android:persistent="true"
-                android:defaultValue="true" />
-            <CheckBoxPreference
-                android:key="next_word_suggestion"
-                android:title="@string/bigram_suggestion"
-                android:summary="@string/bigram_suggestion_summary"
-                android:persistent="true"
-                android:defaultValue="true" />
-            <CheckBoxPreference
-                android:key="next_word_prediction"
-                android:title="@string/bigram_prediction"
-                android:summary="@string/bigram_prediction_summary"
-                android:persistent="true"
-                android:defaultValue="true" />
-            <CheckBoxPreference
-                android:key="enable_span_insert"
-                android:title="@string/enable_span_insert"
-                android:summary="@string/enable_span_insert_summary"
-                android:persistent="true"
-                android:defaultValue="true" />
             <PreferenceScreen
                 android:key="pref_vibration_duration_settings"
                 android:title="@string/prefs_keypress_vibration_duration_settings"/>
             <PreferenceScreen
                 android:key="pref_keypress_sound_volume"
                 android:title="@string/prefs_keypress_sound_volume_settings" />
+            <CheckBoxPreference
+                android:key="pref_gesture_preview_trail"
+                android:title="@string/gesture_preview_trail"
+                android:persistent="true"
+                android:defaultValue="true" />
+            <CheckBoxPreference
+                android:key="pref_show_gesture_floating_preview_text"
+                android:title="@string/gesture_floating_preview_text"
+                android:summary="@string/gesture_floating_preview_text_summary"
+                android:persistent="true"
+                android:defaultValue="false" />
         </PreferenceScreen>
+        <PreferenceScreen
+            android:key="debug_settings"
+            android:title="Debug settings"
+            android:persistent="true"
+            android:defaultValue="false" />
     </PreferenceCategory>
 </PreferenceScreen>
diff --git a/java/res/xml/prefs_for_debug.xml b/java/res/xml/prefs_for_debug.xml
index b926ed0..605a02f 100644
--- a/java/res/xml/prefs_for_debug.xml
+++ b/java/res/xml/prefs_for_debug.xml
@@ -48,4 +48,10 @@
             android:persistent="true"
             android:defaultValue="false"
             />
+
+    <CheckBoxPreference
+            android:key="usability_study_mode"
+            android:title="@string/prefs_usability_study_mode"
+            android:persistent="true"
+            android:defaultValue="false" />
 </PreferenceScreen>
diff --git a/java/res/xml/rowkeys_arabic1.xml b/java/res/xml/rowkeys_arabic1.xml
index b1bf790..a4bef83 100644
--- a/java/res/xml/rowkeys_arabic1.xml
+++ b/java/res/xml/rowkeys_arabic1.xml
@@ -26,13 +26,15 @@
     <Key
         latin:keyLabel="&#x0636;"
         latin:keyHintLabel="1"
-        latin:additionalMoreKeys="1,&#x0661;" />
+        latin:additionalMoreKeys="1,&#x0661;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0635: "ص" ARABIC LETTER SAD
          U+0662: "٢" ARABIC-INDIC DIGIT TWO -->
     <Key
         latin:keyLabel="&#x0635;"
         latin:keyHintLabel="2"
-        latin:additionalMoreKeys="2,&#x0662;" />
+        latin:additionalMoreKeys="2,&#x0662;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0642: "ق" ARABIC LETTER QAF
          U+06A8: "ڨ" ARABIC LETTER QAF WITH THREE DOTS ABOVE
          U+0663: "٣" ARABIC-INDIC DIGIT THREE -->
@@ -41,7 +43,8 @@
         latin:keyLabel="&#x0642;"
         latin:keyHintLabel="3"
         latin:additionalMoreKeys="3,&#x0663;"
-        latin:moreKeys="&#x06A8;" />
+        latin:moreKeys="&#x06A8;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0641: "ف" ARABIC LETTER FEH
          U+06A4: "ڤ" ARABIC LETTER VEH
          U+06A2: "ڢ" ARABIC LETTER FEH WITH DOT MOVED BELOW
@@ -53,19 +56,22 @@
         latin:keyLabel="&#x0641;"
         latin:keyHintLabel="4"
         latin:additionalMoreKeys="4,&#x0664;"
-        latin:moreKeys="&#x06A4;,&#x06A2;,&#x06A5;" />
+        latin:moreKeys="&#x06A4;,&#x06A2;,&#x06A5;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+063A: "غ" ARABIC LETTER GHAIN
          U+0665: "٥" ARABIC-INDIC DIGIT FIVE -->
     <Key
         latin:keyLabel="&#x063A;"
         latin:keyHintLabel="5"
-        latin:additionalMoreKeys="5,&#x0665;" />
+        latin:additionalMoreKeys="5,&#x0665;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0639: "ع" ARABIC LETTER AIN
          U+0666: "٦" ARABIC-INDIC DIGIT SIX -->
     <Key
         latin:keyLabel="&#x0639;"
         latin:keyHintLabel="6"
-        latin:additionalMoreKeys="6,&#x0666;" />
+        latin:additionalMoreKeys="6,&#x0666;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0647: "ه" ARABIC LETTER HEH
          U+FEEB: "ﻫ" ARABIC LETTER HEH INITIAL FORM
          U+0647 U+200D: ARABIC LETTER HEH + ZERO WIDTH JOINER
@@ -74,19 +80,22 @@
         latin:keyLabel="&#x0647;"
         latin:keyHintLabel="7"
         latin:additionalMoreKeys="7,&#x0667;"
-        latin:moreKeys="&#xFEEB;|&#x0647;&#x200D;" />
+        latin:moreKeys="&#xFEEB;|&#x0647;&#x200D;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062E: "خ" ARABIC LETTER KHAH
          U+0668: "٨" ARABIC-INDIC DIGIT EIGHT -->
     <Key
         latin:keyLabel="&#x062E;"
         latin:keyHintLabel="8"
-        latin:additionalMoreKeys="8,&#x0668;" />
+        latin:additionalMoreKeys="8,&#x0668;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062D: "ح" ARABIC LETTER HAH
          U+0669: "٩" ARABIC-INDIC DIGIT NINE -->
     <Key
         latin:keyLabel="&#x062D;"
         latin:keyHintLabel="9"
-        latin:additionalMoreKeys="9,&#x0669;" />
+        latin:additionalMoreKeys="9,&#x0669;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062C: "ج" ARABIC LETTER JEEM
          U+0686: "چ" ARABIC LETTER TCHEH
          U+0660: "٠" ARABIC-INDIC DIGIT ZERO -->
@@ -94,5 +103,6 @@
         latin:keyLabel="&#x062C;"
         latin:keyHintLabel="0"
         latin:additionalMoreKeys="0,&#x0660;"
-        latin:moreKeys="&#x0686;" />
+        latin:moreKeys="&#x0686;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml/rowkeys_arabic2.xml b/java/res/xml/rowkeys_arabic2.xml
index f86aae0..d733f64 100644
--- a/java/res/xml/rowkeys_arabic2.xml
+++ b/java/res/xml/rowkeys_arabic2.xml
@@ -26,21 +26,25 @@
     <!-- TODO: DroidSansArabic lacks the glyph of U+069C ARABIC LETTER SEEN WITH THREE DOTS BELOW AND THREE DOTS ABOVE -->
     <Key
         latin:keyLabel="&#x0634;"
-        latin:moreKeys="&#x069C;" />
+        latin:moreKeys="&#x069C;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0633: "س" ARABIC LETTER SEEN -->
     <Key
-        latin:keyLabel="&#x0633;" />
+        latin:keyLabel="&#x0633;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+064A: "ي" ARABIC LETTER YEH
          U+0626: "ئ" ARABIC LETTER YEH WITH HAMZA ABOVE
          U+0649: "ى" ARABIC LETTER ALEF MAKSURA -->
     <Key
         latin:keyLabel="&#x064A;"
-        latin:moreKeys="&#x0626;,&#x0649;" />
+        latin:moreKeys="&#x0626;,&#x0649;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0628: "ب" ARABIC LETTER BEH
          U+067E: "پ" ARABIC LETTER PEH -->
     <Key
         latin:keyLabel="&#x0628;"
-        latin:moreKeys="&#x067E;" />
+        latin:moreKeys="&#x067E;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0644: "ل" ARABIC LETTER LAM
          U+FEFB: "ﻻ" ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM
          U+0627: "ا" ARABIC LETTER ALEF
@@ -52,7 +56,8 @@
          U+0622: "آ" ARABIC LETTER ALEF WITH MADDA ABOVE -->
     <Key
         latin:keyLabel="&#x0644;"
-        latin:moreKeys="&#xFEFB;|&#x0644;&#x0627;,&#xFEF7;|&#x0644;&#x0623;,&#xFEF9;|&#x0644;&#x0625;,&#xFEF5;|&#x0644;&#x0622;" />
+        latin:moreKeys="&#xFEFB;|&#x0644;&#x0627;,&#xFEF7;|&#x0644;&#x0623;,&#xFEF9;|&#x0644;&#x0625;,&#xFEF5;|&#x0644;&#x0622;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0627: "ا" ARABIC LETTER ALEF
          U+0621: "ء" ARABIC LETTER HAMZA
          U+0671: "ٱ" ARABIC LETTER ALEF WASLA
@@ -61,23 +66,27 @@
          U+0622: "آ" ARABIC LETTER ALEF WITH MADDA ABOVE -->
     <Key
         latin:keyLabel="&#x0627;"
-        latin:moreKeys="&#x0621;,&#x0671;,&#x0623;,&#x0625;,&#x0622;" />
+        latin:moreKeys="&#x0621;,&#x0671;,&#x0623;,&#x0625;,&#x0622;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062A: "ت" ARABIC LETTER TEH
          U+062B: "ﺙ" ARABIC LETTER THEH -->
     <Key
         latin:keyLabel="&#x062A;"
-        latin:moreKeys="&#x062B;" />
+        latin:moreKeys="&#x062B;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0646: "ن" ARABIC LETTER NOON -->
     <Key
-        latin:keyLabel="&#x0646;" />
+        latin:keyLabel="&#x0646;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0645: "م" ARABIC LETTER MEEM -->
     <Key
-        latin:keyLabel="&#x0645;" />
+        latin:keyLabel="&#x0645;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0643: "ك" ARABIC LETTER KAF
          U+06AF: "گ" ARABIC LETTER GAF
          U+06A9: "ک" ARABIC LETTER KEHEH -->
     <Key
         latin:keyLabel="&#x0643;"
         latin:moreKeys="&#x06AF;,&#x06A9;"
-        latin:keyWidth="fillRight" />
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml/rowkeys_arabic3.xml b/java/res/xml/rowkeys_arabic3.xml
index 9e9eac0..e4e6948 100644
--- a/java/res/xml/rowkeys_arabic3.xml
+++ b/java/res/xml/rowkeys_arabic3.xml
@@ -23,30 +23,38 @@
 >
     <!-- U+0638: "ظ" ARABIC LETTER ZAH -->
     <Key
-        latin:keyLabel="&#x0638;" />
+        latin:keyLabel="&#x0638;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0637: "ط" ARABIC LETTER TAH -->
     <Key
-        latin:keyLabel="&#x0637;" />
+        latin:keyLabel="&#x0637;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0630: "ذ" ARABIC LETTER THAL -->
     <Key
-        latin:keyLabel="&#x0630;" />
+        latin:keyLabel="&#x0630;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062F: "د" ARABIC LETTER DAL -->
     <Key
-        latin:keyLabel="&#x062F;" />
+        latin:keyLabel="&#x062F;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0632: "ز" ARABIC LETTER ZAIN
          U+0698: "ژ" ARABIC LETTER JEH -->
     <Key
         latin:keyLabel="&#x0632;"
-        latin:moreKeys="&#x0698;" />
+        latin:moreKeys="&#x0698;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0631: "ر" ARABIC LETTER REH -->
     <Key
-        latin:keyLabel="&#x0631;" />
+        latin:keyLabel="&#x0631;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0629: "ة" ARABIC LETTER TEH MARBUTA -->
     <Key
-        latin:keyLabel="&#x0629;" />
+        latin:keyLabel="&#x0629;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0648: "و" ARABIC LETTER WAW
          U+0624: "ﺅ" ARABIC LETTER WAW WITH HAMZA ABOVE -->
     <Key
         latin:keyLabel="&#x0648;"
-        latin:moreKeys="&#x0624;" />
+        latin:moreKeys="&#x0624;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml/rowkeys_east_slavic1.xml b/java/res/xml/rowkeys_east_slavic1.xml
index 00cb6a9..c1b43bd 100644
--- a/java/res/xml/rowkeys_east_slavic1.xml
+++ b/java/res/xml/rowkeys_east_slavic1.xml
@@ -47,7 +47,7 @@
         latin:keyLabel="&#x0435;"
         latin:keyHintLabel="5"
         latin:additionalMoreKeys="5"
-        latin:moreKeys="!text/more_keys_for_cyrillic_ye" />
+        latin:moreKeys="!text/more_keys_for_cyrillic_ie" />
     <!-- U+043D: "н" CYRILLIC SMALL LETTER EN -->
     <Key
         latin:keyLabel="&#x043D;"
@@ -58,7 +58,8 @@
     <Key
         latin:keyLabel="&#x0433;"
         latin:keyHintLabel="7"
-        latin:additionalMoreKeys="7" />
+        latin:additionalMoreKeys="7"
+        latin:moreKeys="!text/more_keys_for_cyrillic_ghe" />
     <!-- U+0448: "ш" CYRILLIC SMALL LETTER SHA -->
     <Key
         latin:keyLabel="&#x0448;"
@@ -75,6 +76,5 @@
         latin:additionalMoreKeys="0" />
     <!-- U+0445: "х" CYRILLIC SMALL LETTER HA -->
     <Key
-        latin:keyLabel="&#x0445;"
-        latin:moreKeys="!text/more_keys_for_cyrillic_ha" />
+        latin:keyLabel="&#x0445;" />
 </merge>
diff --git a/java/res/xml/rowkeys_east_slavic2.xml b/java/res/xml/rowkeys_east_slavic2.xml
index c635af2..9743727 100644
--- a/java/res/xml/rowkeys_east_slavic2.xml
+++ b/java/res/xml/rowkeys_east_slavic2.xml
@@ -52,7 +52,6 @@
     <!-- U+0436: "ж" CYRILLIC SMALL LETTER ZHE -->
     <Key
         latin:keyLabel="&#x0436;" />
-    <!-- U+044D: "э" CYRILLIC SMALL LETTER E -->
     <Key
-        latin:keyLabel="&#x044D;" />
+        latin:keyLabel="!text/keylabel_for_east_slavic_row2_11" />
 </merge>
diff --git a/java/res/xml/rowkeys_esperanto1.xml b/java/res/xml/rowkeys_esperanto1.xml
deleted file mode 100644
index 6994d4b..0000000
--- a/java/res/xml/rowkeys_esperanto1.xml
+++ /dev/null
@@ -1,125 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <!-- U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX -->
-    <Key
-        latin:keyLabel="&#x015D;"
-        latin:keyHintLabel="1"
-        latin:additionalMoreKeys="1"
-        latin:moreKeys="q" />
-    <!-- U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX
-         U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX -->
-    <Key
-        latin:keyLabel="&#x011D;"
-        latin:keyHintLabel="2"
-        latin:additionalMoreKeys="2"
-        latin:moreKeys="w,&#x0175;" />
-    <!-- U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
-         U+011B: "ě" LATIN SMALL LETTER E WITH CARON
-         U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
-         U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
-         U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
-         U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
-         U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
-         U+0113: "ē" LATIN SMALL LETTER E WITH MACRON -->
-    <Key
-        latin:keyLabel="e"
-        latin:keyHintLabel="3"
-        latin:additionalMoreKeys="3"
-        latin:moreKeys="&#x00E9;,&#x011B;,&#x00E8;,&#x00EA;,&#x00EB;,&#x0119;,&#x0117;,&#x0113;" />
-    <!-- U+0159: "ř" LATIN SMALL LETTER R WITH CARON
-         U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
-         U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA  -->
-    <Key
-        latin:keyLabel="r"
-        latin:keyHintLabel="4"
-        latin:additionalMoreKeys="4"
-        latin:moreKeys="&#x0159;,&#x0155;,&#x0157;" />
-    <!-- U+0165: "ť" LATIN SMALL LETTER T WITH CARON
-         U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW
-         U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
-         U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE -->
-    <Key
-        latin:keyLabel="t"
-        latin:keyHintLabel="5"
-        latin:additionalMoreKeys="5"
-        latin:moreKeys="&#x0165;,&#x021B;,&#x0163;,&#x0167;" />
-    <!-- U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE
-         U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
-         U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX
-         U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
-         U+00FE: "þ" LATIN SMALL LETTER THORN -->
-    <Key
-        latin:keyLabel="&#x016D;"
-        latin:keyHintLabel="6"
-        latin:additionalMoreKeys="6"
-        latin:moreKeys="y,&#x00FD;,&#x0177;,&#x00FF;,&#x00FE;" />
-    <!-- U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
-         U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
-         U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
-         U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
-         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
-         U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
-         U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE
-         U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
-         U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
-         U+00B5: "µ" MICRO SIGN -->
-    <Key
-        latin:keyLabel="u"
-        latin:keyHintLabel="7"
-        latin:additionalMoreKeys="7"
-        latin:moreKeys="&#x00FA;,&#x016F;,&#x00FB;,&#x00FC;,&#x00F9;,&#x016B;,&#x0169;,&#x0171;,&#x0173;,&#x00B5;" />
-    <!-- U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
-         U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
-         U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
-         U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE
-         U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
-         U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
-         U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
-         U+0131: "ı" LATIN SMALL LETTER DOTLESS I
-         U+0133: "ij" LATIN SMALL LIGATURE IJ -->
-    <Key
-        latin:keyLabel="i"
-        latin:keyHintLabel="8"
-        latin:additionalMoreKeys="8"
-        latin:moreKeys="&#x00ED;,&#x00EE;,&#x00EF;,&#x0129;,&#x00EC;,&#x012F;,&#x012B;,&#x0131;,&#x0133;" />
-    <!-- U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
-         U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
-         U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
-         U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
-         U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
-         U+0153: "œ" LATIN SMALL LIGATURE OE
-         U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
-         U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
-         U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
-         U+00BA: "º" MASCULINE ORDINAL INDICATOR -->
-    <Key
-        latin:keyLabel="o"
-        latin:keyHintLabel="9"
-        latin:additionalMoreKeys="9"
-        latin:moreKeys="&#x00F3;,&#x00F6;,&#x00F4;,&#x00F2;,&#x00F5;,&#x0153;,&#x00F8;,&#x014D;,&#x0151;,&#x00BA;" />
-    <Key
-        latin:keyLabel="p"
-        latin:keyHintLabel="0"
-        latin:additionalMoreKeys="0" />
-</merge>
diff --git a/java/res/xml/rowkeys_esperanto2.xml b/java/res/xml/rowkeys_esperanto2.xml
deleted file mode 100644
index ebc968a..0000000
--- a/java/res/xml/rowkeys_esperanto2.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <!-- U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
-         U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
-         U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
-         U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
-         U+00E6: "æ" LATIN SMALL LETTER AE
-         U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
-         U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
-         U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
-         U+0103: "ă" LATIN SMALL LETTER A WITH BREVE
-         U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
-         U+00AA: "ª" FEMININE ORDINAL INDICATOR -->
-    <Key
-        latin:keyLabel="a"
-        latin:moreKeys="&#x00E1;,&#x00E0;,&#x00E2;,&#x00E4;,&#x00E6;,&#x00E3;,&#x00E5;,&#x0101;,&#x0103;,&#x0105;,&#x00AA;" />
-    <!-- U+00DF: "ß" LATIN SMALL LETTER SHARP S
-         U+0161: "š" LATIN SMALL LETTER S WITH CARON
-         U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
-         U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW
-         U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA -->
-    <Key
-        latin:keyLabel="s"
-        latin:moreKeys="&#x00DF;,&#x0161;,&#x015B;,&#x0219;,&#x015F;" />
-    <!-- U+00F0: "ð" LATIN SMALL LETTER ETH
-         U+010F: "ď" LATIN SMALL LETTER D WITH CARON
-         U+0111: "đ" LATIN SMALL LETTER D WITH STROKE -->
-    <Key
-        latin:keyLabel="d"
-        latin:moreKeys="&#x00F0;,&#x010F;,&#x0111;" />
-    <Key
-        latin:keyLabel="f" />
-    <!-- U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
-         U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE
-         U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA -->
-    <Key
-        latin:keyLabel="g"
-        latin:moreKeys="&#x011F;,&#x0121;,&#x0123;" />
-    <!-- U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX
-         U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE -->
-    <Key
-        latin:keyLabel="h"
-        latin:moreKeys="&#x0125;,&#x0127;" />
-    <Key
-        latin:keyLabel="j" />
-    <!-- U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
-         U+0138: "ĸ" LATIN SMALL LETTER KRA  -->
-    <Key
-        latin:keyLabel="k"
-        latin:moreKeys="&#x0137;,&#x0138;" />
-    <!-- U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
-         U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
-         U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
-         U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT
-         U+0142: "ł" LATIN SMALL LETTER L WITH STROKE -->
-    <Key
-        latin:keyLabel="l"
-        latin:moreKeys="&#x013A;,&#x013C;,&#x013E;,&#x0140;,&#x0142;" />
-    <!-- U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX -->
-    <Key
-        latin:keyLabel="&#x0135;" />
-</merge>
diff --git a/java/res/xml/rowkeys_esperanto3.xml b/java/res/xml/rowkeys_esperanto3.xml
deleted file mode 100644
index b2eab8d..0000000
--- a/java/res/xml/rowkeys_esperanto3.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <!-- U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
-         U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
-         U+017E: "ž" LATIN SMALL LETTER Z WITH CARON -->
-    <Key
-        latin:keyLabel="z"
-        latin:moreKeys="&#x017A;,&#x017C;,&#x017E;" />
-    <!-- U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX -->
-    <Key
-        latin:keyLabel="&#x0109;"
-        latin:moreKeys="x" />
-    <!-- U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
-         U+010D: "č" LATIN SMALL LETTER C WITH CARON
-         U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
-         U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE -->
-    <Key
-        latin:keyLabel="c"
-        latin:moreKeys="&#x0107;,&#x010D;,&#x00E7;,&#x010B;" />
-    <!-- U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX -->
-    <Key
-        latin:keyLabel="v"
-        latin:moreKeys="w,&#x0175;" />
-    <Key
-        latin:keyLabel="b" />
-    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
-         U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
-         U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
-         U+0148: "ň" LATIN SMALL LETTER N WITH CARON
-         U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
-         U+014B: "ŋ" LATIN SMALL LETTER ENG -->
-    <Key
-        latin:keyLabel="n"
-        latin:moreKeys="&#x00F1;,&#x0144;,&#x0146;,&#x0148;,&#x0149;,&#x014B;" />
-    <Key
-        latin:keyLabel="m" />
-</merge>
diff --git a/java/res/xml/rowkeys_farsi1.xml b/java/res/xml/rowkeys_farsi1.xml
index 840b048..0ccf1ab 100644
--- a/java/res/xml/rowkeys_farsi1.xml
+++ b/java/res/xml/rowkeys_farsi1.xml
@@ -28,31 +28,36 @@
         latin:keyLabel="&#x0635;"
         latin:moreKeys="&#x0636;,%"
         latin:keyHintLabel="&#x06F1;"
-        latin:additionalMoreKeys="&#x06F1;,1" />
+        latin:additionalMoreKeys="&#x06F1;,1"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0642: "ق" ARABIC LETTER QAF
          U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO -->
     <Key
         latin:keyLabel="&#x0642;"
         latin:keyHintLabel="&#x06F2;"
-        latin:additionalMoreKeys="&#x06F2;,2" />
+        latin:additionalMoreKeys="&#x06F2;,2"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0641: "ف" ARABIC LETTER FEH
          U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE -->
     <Key
         latin:keyLabel="&#x0641;"
         latin:keyHintLabel="&#x06F3;"
-        latin:additionalMoreKeys="&#x06F3;,3" />
+        latin:additionalMoreKeys="&#x06F3;,3"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+063A: "غ" ARABIC LETTER GHAIN
          U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR -->
     <Key
         latin:keyLabel="&#x063A;"
         latin:keyHintLabel="&#x06F4;"
-        latin:additionalMoreKeys="&#x06F4;,4" />
+        latin:additionalMoreKeys="&#x06F4;,4"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0639: "ع" ARABIC LETTER AIN
          U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE -->
     <Key
         latin:keyLabel="&#x0639;"
         latin:keyHintLabel="&#x06F5;"
-        latin:additionalMoreKeys="&#x06F5;,5" />
+        latin:additionalMoreKeys="&#x06F5;,5"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0647: "ه" ARABIC LETTER HEH
          U+FEEB: "ﻫ" ARABIC LETTER HEH INITIAL FORM
          U+0647/U+200D: ARABIC LETTER HEH + ZERO WIDTH JOINER
@@ -63,29 +68,34 @@
         latin:keyLabel="&#x0647;"
         latin:moreKeys="&#xFEEB;|&#x0647;&#x200D;,&#x0647;&#x0654;,&#x0629;,%"
         latin:keyHintLabel="&#x06F6;"
-        latin:additionalMoreKeys="&#x06F6;,6" />
+        latin:additionalMoreKeys="&#x06F6;,6"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062E: "خ" ARABIC LETTER KHAH
          U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN -->
     <Key
         latin:keyLabel="&#x062E;"
         latin:keyHintLabel="&#x06F7;"
-        latin:additionalMoreKeys="&#x06F7;,7" />
+        latin:additionalMoreKeys="&#x06F7;,7"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062D: "ح" ARABIC LETTER HAH
          U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT -->
     <Key
         latin:keyLabel="&#x062D;"
         latin:keyHintLabel="&#x06F8;"
-        latin:additionalMoreKeys="&#x06F8;,8" />
+        latin:additionalMoreKeys="&#x06F8;,8"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062C: "ج" ARABIC LETTER JEEM
          U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE -->
     <Key
         latin:keyLabel="&#x062C;"
         latin:keyHintLabel="&#x06F9;"
-        latin:additionalMoreKeys="&#x06F9;,9" />
+        latin:additionalMoreKeys="&#x06F9;,9"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0686: "چ" ARABIC LETTER TCHEH
          U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO -->
     <Key
         latin:keyLabel="&#x0686;"
         latin:keyHintLabel="&#x06F0;"
-        latin:additionalMoreKeys="&#x06F0;,0" />
+        latin:additionalMoreKeys="&#x06F0;,0"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml/rowkeys_farsi2.xml b/java/res/xml/rowkeys_farsi2.xml
index 2154893..4b6abe2 100644
--- a/java/res/xml/rowkeys_farsi2.xml
+++ b/java/res/xml/rowkeys_farsi2.xml
@@ -23,12 +23,14 @@
 >
     <!-- U+0634: "ش" ARABIC LETTER SHEEN -->
     <Key
-        latin:keyLabel="&#x0634;" />
+        latin:keyLabel="&#x0634;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0633: "س" ARABIC LETTER SEEN
          U+0636: "ض" ARABIC LETTER DAD -->
     <Key
         latin:keyLabel="&#x0633;"
-        latin:moreKeys="&#x0636;" />
+        latin:moreKeys="&#x0636;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+06CC: "ی" ARABIC LETTER FARSI YEH
          U+0626: "ئ" ARABIC LETTER YEH WITH HAMZA ABOVE
          U+064A: "ي" ARABIC LETTER YEH
@@ -36,13 +38,16 @@
          U+0649: "ى" ARABIC LETTER ALEF MAKSURA -->
     <Key
         latin:keyLabel="&#x06CC;"
-        latin:moreKeys="&#x0626;,&#x064A;,&#xFBE8;|&#x0649;" />
+        latin:moreKeys="&#x0626;,&#x064A;,&#xFBE8;|&#x0649;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0628: "ب" ARABIC LETTER BEH -->
     <Key
-        latin:keyLabel="&#x0628;" />
+        latin:keyLabel="&#x0628;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0644: "ل" ARABIC LETTER LAM -->
     <Key
-        latin:keyLabel="&#x0644;" />
+        latin:keyLabel="&#x0644;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0627: "ا" ARABIC LETTER ALEF
          U+0621: "ء" ARABIC LETTER HAMZA
          U+0622: "آ" ARABIC LETTER ALEF WITH MADDA ABOVE
@@ -51,22 +56,27 @@
          U+0625: "إ" ARABIC LETTER ALEF WITH HAMZA BELOW -->
     <Key
         latin:keyLabel="&#x0627;"
-        latin:moreKeys="&#x0621;,&#x0622;,&#x0623;,&#x0671;,&#x0625;" />
+        latin:moreKeys="&#x0621;,&#x0622;,&#x0623;,&#x0671;,&#x0625;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062A: "ت" ARABIC LETTER TEH
          U+062B: "ﺙ" ARABIC LETTER THEH
          U+0629: "ة": ARABIC LETTER TEH MARBUTA -->
     <Key
         latin:keyLabel="&#x062A;"
-        latin:moreKeys="&#x062B;,&#x0629;" />
+        latin:moreKeys="&#x062B;,&#x0629;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0646: "ن" ARABIC LETTER NOON -->
     <Key
-        latin:keyLabel="&#x0646;" />
+        latin:keyLabel="&#x0646;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0645: "م" ARABIC LETTER MEEM -->
     <Key
-        latin:keyLabel="&#x0645;" />
+        latin:keyLabel="&#x0645;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+06A9: "ک" ARABIC LETTER KEHEH
          U+0643: "ك" ARABIC LETTER KAF -->
     <Key
         latin:keyLabel="&#x06A9;"
-        latin:moreKeys="&#x0643;" />
+        latin:moreKeys="&#x0643;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml/rowkeys_farsi3.xml b/java/res/xml/rowkeys_farsi3.xml
index 29c3513..7d2e81f 100644
--- a/java/res/xml/rowkeys_farsi3.xml
+++ b/java/res/xml/rowkeys_farsi3.xml
@@ -25,30 +25,38 @@
          U+0638: "ظ" ARABIC LETTER ZAH -->
     <Key
         latin:keyLabel="&#x0637;"
-        latin:moreKeys="&#x0638;" />
+        latin:moreKeys="&#x0638;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0632: "ز" ARABIC LETTER ZAIN
          U+0698: "ژ" ARABIC LETTER JEH -->
     <Key
         latin:keyLabel="&#x0632;"
-        latin:moreKeys="&#x0698;" />
+        latin:moreKeys="&#x0698;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0631: "ر" ARABIC LETTER REH -->
     <Key
-        latin:keyLabel="&#x0631;" />
+        latin:keyLabel="&#x0631;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0630: "ذ" ARABIC LETTER THAL -->
     <Key
-        latin:keyLabel="&#x0630;" />
+        latin:keyLabel="&#x0630;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+062F: "د" ARABIC LETTER DAL -->
     <Key
-        latin:keyLabel="&#x062F;" />
+        latin:keyLabel="&#x062F;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+067E: "پ" ARABIC LETTER PEH -->
     <Key
-        latin:keyLabel="&#x067E;" />
+        latin:keyLabel="&#x067E;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+0648: "و" ARABIC LETTER WAW
          U+0624: "ؤ" ARABIC LETTER WAW WITH HAMZA ABOVE -->
     <Key
         latin:keyLabel="&#x0648;"
-        latin:moreKeys="&#x0624;" />
+        latin:moreKeys="&#x0624;"
+        latin:keyLabelFlags="fontNormal" />
     <!-- U+06AF: "گ" ARABIC LETTER GAF -->
     <Key
-        latin:keyLabel="&#x06AF;" />
+        latin:keyLabel="&#x06AF;"
+        latin:keyLabelFlags="fontNormal" />
 </merge>
diff --git a/java/res/xml/rowkeys_hindi1.xml b/java/res/xml/rowkeys_hindi1.xml
index 656ba01..1120804 100644
--- a/java/res/xml/rowkeys_hindi1.xml
+++ b/java/res/xml/rowkeys_hindi1.xml
@@ -29,50 +29,61 @@
                  U+0912/U+0902: "ऒं" DEVANAGARI LETTER SHORT O//DEVANAGARI SIGN ANUSVARA -->
             <Key
                 latin:keyLabel="&#x0914;"
-                latin:moreKeys="&#x0912;&#x0902;" />
+                latin:moreKeys="&#x0912;&#x0902;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0910: "ऐ" DEVANAGARI LETTER AI
                  U+0910/U+0902: "ऐं" DEVANAGARI LETTER AI/DEVANAGARI SIGN ANUSVARA -->
             <Key
                 latin:keyLabel="&#x0910;"
-                latin:moreKeys="&#x0910;&#x0902;" />
+                latin:moreKeys="&#x0910;&#x0902;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0906: "आ" DEVANAGARI LETTER AA
                  U+0906/U+0902: "आं" DEVANAGARI LETTER AA/DEVANAGARI SIGN ANUSVARA
                  U+0906/U+0901: "आँ" DEVANAGARI LETTER AA/DEVANAGARI SIGN CANDRABINDU -->
             <Key
                 latin:keyLabel="&#x0906;"
-                latin:moreKeys="&#x0906;&#x0902;,&#x0906;&#x0901;" />
+                latin:moreKeys="&#x0906;&#x0902;,&#x0906;&#x0901;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0908: "ई" DEVANAGARI LETTER II
                  U+0908/U+0902: "ईं" DEVANAGARI LETTER II/DEVANAGARI SIGN ANUSVARA -->
             <Key
                 latin:keyLabel="&#x0908;"
-                latin:moreKeys="&#x0908;&#x0902;" />
+                latin:moreKeys="&#x0908;&#x0902;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+090A: "ऊ" DEVANAGARI LETTER UU
                  U+090A/U+0902: "ऊं" DEVANAGARI LETTER UU/DEVANAGARI SIGN ANUSVARA
                  U+090A/U+0901: "ऊँ" DEVANAGARI LETTER UU/DEVANAGARI SIGN CANDRABINDU -->
             <Key
                 latin:keyLabel="&#x090A;"
-                latin:moreKeys="&#x090A;&#x0902;,&#x090A;&#x0901;" />
+                latin:moreKeys="&#x090A;&#x0902;,&#x090A;&#x0901;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+092D: "भ" DEVANAGARI LETTER BHA -->
             <Key
-                latin:keyLabel="&#x092D;" />
+                latin:keyLabel="&#x092D;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0903: "ः" DEVANAGARI SIGN VISARGA -->
             <Key
-                latin:keyLabel="&#x0903;" />
+                latin:keyLabel="&#x0903;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0918: "घ" DEVANAGARI LETTER GHA -->
             <Key
-                latin:keyLabel="&#x0918;" />
+                latin:keyLabel="&#x0918;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0927: "ध" DEVANAGARI LETTER DHA
                  U+0915/U+094D/U+0937: "क्ष" DEVANAGARI LETTER KA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER SSA
                  U+0936/U+094D/U+0930: "श्र" DEVANAGARI LETTER SHA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER RA -->
             <Key
                 latin:keyLabel="&#x0927;"
-                latin:moreKeys="&#x0915;&#x094D;&#x0937;,&#x0936;&#x094D;&#x0930;" />
+                latin:moreKeys="&#x0915;&#x094D;&#x0937;,&#x0936;&#x094D;&#x0930;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+091D: "झ" DEVANAGARI LETTER JHA -->
             <Key
-                latin:keyLabel="&#x091D;" />
+                latin:keyLabel="&#x091D;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0922: "ढ" DEVANAGARI LETTER DDHA -->
             <Key
-                latin:keyLabel="&#x0922;" />
+                latin:keyLabel="&#x0922;"
+                latin:keyLabelFlags="fontNormal" />
         </case>
         <default>
             <!-- U+094C: "ौ" DEVANAGARI VOWEL SIGN AU
@@ -82,7 +93,8 @@
                 latin:keyLabel="&#x094C;"
                 latin:moreKeys="&#x094C;&#x0902;,%"
                 latin:keyHintLabel="1"
-                latin:additionalMoreKeys="&#x0967;,1" />
+                latin:additionalMoreKeys="&#x0967;,1"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0948: "ै" DEVANAGARI VOWEL SIGN AI
                  U+0948/U+0902: "ैं" DEVANAGARI VOWEL SIGN AI/DEVANAGARI SIGN ANUSVARA
                  U+0968: "२" DEVANAGARI DIGIT TWO -->
@@ -90,7 +102,8 @@
                 latin:keyLabel="&#x0948;"
                 latin:moreKeys="&#x0948;&#x0902;,%"
                 latin:keyHintLabel="2"
-                latin:additionalMoreKeys="&#x0968;,2" />
+                latin:additionalMoreKeys="&#x0968;,2"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+093E: "ा" DEVANAGARI VOWEL SIGN AA
                  U+093E/U+0902: "ां" DEVANAGARI VOWEL SIGN AA/DEVANAGARI SIGN ANUSVARA
                  U+093E/U+0901: "ाँ" DEVANAGARI VOWEL SIGN AA/DEVANAGARI SIGN CANDRABINDU
@@ -99,7 +112,8 @@
                 latin:keyLabel="&#x093E;"
                 latin:moreKeys="&#x093E;&#x0902;,&#x093E;&#x0901;,%"
                 latin:keyHintLabel="3"
-                latin:additionalMoreKeys="&#x0969;,3" />
+                latin:additionalMoreKeys="&#x0969;,3"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0940: "ी" DEVANAGARI VOWEL SIGN II
                  U+0940/U+0902: "ीं" DEVANAGARI VOWEL SIGN II/DEVANAGARI SIGN ANUSVARA
                  U+096A: "४" DEVANAGARI DIGIT FOUR -->
@@ -107,7 +121,8 @@
                 latin:keyLabel="&#x0940;"
                 latin:moreKeys="&#x0940;&#x0902;,%"
                 latin:keyHintLabel="4"
-                latin:additionalMoreKeys="&#x096A;,4" />
+                latin:additionalMoreKeys="&#x096A;,4"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0942: "ू" DEVANAGARI VOWEL SIGN UU
                  U+0942/U+0902: "ूं" DEVANAGARI VOWEL SIGN UU/DEVANAGARI SIGN ANUSVARA
                  U+0942/U+0901: "ूँ" DEVANAGARI VOWEL SIGN UU/DEVANAGARI SIGN CANDRABINDU
@@ -116,20 +131,23 @@
                 latin:keyLabel="&#x0942;"
                 latin:moreKeys="&#x0942;&#x0902;,&#x0942;&#x0901;,%"
                 latin:keyHintLabel="5"
-                latin:additionalMoreKeys="&#x096B;,5" />
+                latin:additionalMoreKeys="&#x096B;,5"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+092C: "ब" DEVANAGARI LETTER BA
                  U+092C/U+0952: "ब॒" DEVANAGARI LETTER BA/DEVANAGARI STRESS SIGN ANUDATTA -->
             <Key
                 latin:keyLabel="&#x092C;"
                 latin:moreKeys="&#x092C;&#x0952;,%"
                 latin:keyHintLabel="6"
-                latin:additionalMoreKeys="&#x096C;,6" />
+                latin:additionalMoreKeys="&#x096C;,6"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0939: "ह" DEVANAGARI LETTER HA
                  U+096D: "७" DEVANAGARI DIGIT SEVEN -->
             <Key
                 latin:keyLabel="&#x0939;"
                 latin:keyHintLabel="7"
-                latin:additionalMoreKeys="&#x096D;,7" />
+                latin:additionalMoreKeys="&#x096D;,7"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0917: "ग" DEVANAGARI LETTER GA
                  U+091C/U+094D/U+091E: "ज्ञ" DEVANAGARI LETTER JA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER NYA
                  U+0917/U+093C: "ग़" DEVANAGARI LETTER GA/DEVANAGARI SIGN NUKTA
@@ -139,13 +157,15 @@
                 latin:keyLabel="&#x0917;"
                 latin:moreKeys="&#x091C;&#x094D;&#x091E;,&#x0917;&#x093C;,&#x0917;&#x0952;,%"
                 latin:keyHintLabel="8"
-                latin:additionalMoreKeys="&#x096E;,8" />
+                latin:additionalMoreKeys="&#x096E;,8"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0926: "द" DEVANAGARI LETTER DA
                  U+096F: "९" DEVANAGARI DIGIT NINE -->
             <Key
                 latin:keyLabel="&#x0926;"
                 latin:keyHintLabel="9"
-                latin:additionalMoreKeys="9" />
+                latin:additionalMoreKeys="9"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+091C: "ज" DEVANAGARI LETTER JA
                  U+091C/U+0952: "ज॒" DEVANAGARI LETTER JA/DEVANAGARI STRESS SIGN ANUDATTA
                  U+091C/U+094D/U+091E: "ज्ञ" DEVANAGARI LETTER JA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER NYA
@@ -155,13 +175,15 @@
                 latin:keyLabel="&#x091C;"
                 latin:moreKeys="&#x091C;&#x0952;,&#x091C;&#x094D;&#x091E;,&#x091C;&#x093C;,%"
                 latin:keyHintLabel="0"
-                latin:additionalMoreKeys="&#x0966;,0" />
+                latin:additionalMoreKeys="&#x0966;,0"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0921: "ड" DEVANAGARI LETTER DDA
                  U+0921/U+0952: "ड॒" DEVANAGARI LETTER DDA/DEVANAGARI STRESS SIGN ANUDATTA
                  U+0921/U+093C: "ड़" DEVANAGARI LETTER DDA/DEVANAGARI SIGN NUKTA -->
             <Key
                 latin:keyLabel="&#x0921;"
-                latin:moreKeys="&#x0921;&#x0952;,&#x0921;&#x093C;" />
+                latin:moreKeys="&#x0921;&#x0952;,&#x0921;&#x093C;"
+                latin:keyLabelFlags="fontNormal" />
          </default>
     </switch>
 </merge>
diff --git a/java/res/xml/rowkeys_hindi2.xml b/java/res/xml/rowkeys_hindi2.xml
index 05e3db2..e7c67db 100644
--- a/java/res/xml/rowkeys_hindi2.xml
+++ b/java/res/xml/rowkeys_hindi2.xml
@@ -31,7 +31,8 @@
                  U+0912: "ऒ" DEVANAGARI LETTER SHORT O -->
             <Key
                 latin:keyLabel="&#x0913;"
-                latin:moreKeys="&#x0913;&#x0902;,&#x0911;,&#x0912;" />
+                latin:moreKeys="&#x0913;&#x0902;,&#x0911;,&#x0912;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+090F: "ए" DEVANAGARI LETTER E
                  U+090F/U+0902: "एं" DEVANAGARI LETTER E/DEVANAGARI SIGN ANUSVARA
                  U+090F/U+0901: "एँ" DEVANAGARI LETTER E/DEVANAGARI SIGN CANDRABINDU
@@ -39,50 +40,60 @@
                  U+090E: "ऎ" DEVANAGARI LETTER SHORT E -->
             <Key
                 latin:keyLabel="&#x090F;"
-                latin:moreKeys="&#x090F;&#x0902;,&#x090F;&#x0901;,&#x090D;,&#x090E;" />
+                latin:moreKeys="&#x090F;&#x0902;,&#x090F;&#x0901;,&#x090D;,&#x090E;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0905: "अ" DEVANAGARI LETTER A
                  U+0905/U+0902: "अं" DEVANAGARI LETTER A/DEVANAGARI SIGN ANUSVARA
                  U+0905/U+0901: "अँ" DEVANAGARI LETTER A/DEVANAGARI SIGN CANDRABINDU -->
             <Key
                 latin:keyLabel="&#x0905;"
-                latin:moreKeys="&#x0905;&#x0902;,&#x0905;&#x0901;" />
+                latin:moreKeys="&#x0905;&#x0902;,&#x0905;&#x0901;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0907: "इ" DEVANAGARI LETTER I
                  U+0907/U+0902: "इं" DEVANAGARI LETTER I/DEVANAGARI SIGN ANUSVARA
                  U+0907/U+0901: "इं" DEVANAGARI LETTER I/DEVANAGARI SIGN CANDRABINDU -->
             <Key
                 latin:keyLabel="&#x0907;"
-                latin:moreKeys="&#x0907;&#x0902;,&#x0907;&#x0901;" />
+                latin:moreKeys="&#x0907;&#x0902;,&#x0907;&#x0901;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0909: "उ" DEVANAGARI LETTER U
                  U+0909/U+0902: "उं" DEVANAGARI LETTER U/DEVANAGARI SIGN ANUSVARA
                  U+0909/U+0901: "उँ" DEVANAGARI LETTER U/DEVANAGARI SIGN CANDRABINDU -->
             <Key
                 latin:keyLabel="&#x0909;"
-                latin:moreKeys="&#x0909;&#x0902;,&#x0909;&#x0901;" />
+                latin:moreKeys="&#x0909;&#x0902;,&#x0909;&#x0901;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+092B: "फ" DEVANAGARI LETTER PHA
                  U+092B/U+093C: "फ़" DEVANAGARI LETTER PHA/DEVANAGARI SIGN NUKTA -->
             <Key
                 latin:keyLabel="&#x092B;"
-                latin:moreKeys="&#x092B;&#x093C;" />
+                latin:moreKeys="&#x092B;&#x093C;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0931: "ऱ" DEVANAGARI LETTER RRA
                  U+094D/U+0930: "्र" DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER RA
                  U+0930/U+094D: "र्" DEVANAGARI LETTER RA/DEVANAGARI SIGN VIRAMA -->
             <Key
                 latin:keyLabel="&#x0931;"
-                latin:moreKeys="&#x094D;&#x0930;,&#x0930;&#x094D;" />
+                latin:moreKeys="&#x094D;&#x0930;,&#x0930;&#x094D;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0916: "ख" DEVANAGARI LETTER KHA
                  U+0916/U+093C: "ख़" DEVANAGARI LETTER KHA/DEVANAGARI SIGN NUKTA -->
             <Key
                 latin:keyLabel="&#x0916;"
-                latin:moreKeys="&#x0916;&#x093C;" />
+                latin:moreKeys="&#x0916;&#x093C;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0925: "थ" DEVANAGARI LETTER THA -->
             <Key
-                latin:keyLabel="&#x0925;" />
+                latin:keyLabel="&#x0925;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+091B: "छ" DEVANAGARI LETTER CHA -->
             <Key
-                latin:keyLabel="&#x091B;" />
+                latin:keyLabel="&#x091B;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0920: "ठ" DEVANAGARI LETTER TTHA -->
             <Key
-                latin:keyLabel="&#x0920;" />
+                latin:keyLabel="&#x0920;"
+                latin:keyLabelFlags="fontNormal" />
         </case>
         <default>
             <!-- U+094B: "ो" DEVANAGARI VOWEL SIGN O
@@ -91,52 +102,63 @@
                  U+094A: "ॊ" DEVANAGARI VOWEL SIGN SHORT O -->
             <Key
                 latin:keyLabel="&#x094B;"
-                latin:moreKeys="&#x094B;&#x0902;,&#x0949;,&#x094A;" />
+                latin:moreKeys="&#x094B;&#x0902;,&#x0949;,&#x094A;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0947: "े" DEVANAGARI VOWEL SIGN E
                  U+0947/U+0902: "ें" DEVANAGARI VOWEL SIGN E/DEVANAGARI SIGN ANUSVARA -->
             <Key
                 latin:keyLabel="&#x0947;"
-                latin:moreKeys="&#x0947;&#x0902;" />
+                latin:moreKeys="&#x0947;&#x0902;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+094D: "्" DEVANAGARI SIGN VIRAMA -->
             <Key
-                latin:keyLabel="&#x094D;" />
+                latin:keyLabel="&#x094D;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+093F: "ि" DEVANAGARI VOWEL SIGN I
                  U+093F/U+0902: "िं" DEVANAGARI VOWEL SIGN I/DEVANAGARI SIGN ANUSVARA -->
             <Key
                 latin:keyLabel="&#x093F;"
-                latin:moreKeys="&#x093F;&#x0902;" />
+                latin:moreKeys="&#x093F;&#x0902;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0941: "ु" DEVANAGARI VOWEL SIGN U
                  U+0941/U+0902: "ुं" DEVANAGARI VOWEL SIGN U/DEVANAGARI SIGN ANUSVARA
                  U+0941/U+0901: "ुँ" DEVANAGARI VOWEL SIGN U/DEVANAGARI SIGN CANDRABINDU -->
             <Key
                 latin:keyLabel="&#x0941;"
-                latin:moreKeys="&#x0941;&#x0902;,&#x0941;&#x0901;" />
+                latin:moreKeys="&#x0941;&#x0902;,&#x0941;&#x0901;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+092A: "प" DEVANAGARI LETTER PA -->
             <Key
-                latin:keyLabel="&#x092A;" />
+                latin:keyLabel="&#x092A;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0930: "र" DEVANAGARI LETTER RA
                  U+090B: "ऋ" DEVANAGARI LETTER VOCALIC R
                  U+0930/U+093C: "ऱ" DEVANAGARI LETTER RA/DEVANAGARI SIGN NUKTA
                  U+0960: "ॠ" DEVANAGARI LETTER VOCALIC RR -->
             <Key
                 latin:keyLabel="&#x0930;"
-                latin:moreKeys="&#x090B;,&#x0930;&#x093C;,&#x0960;" />
+                latin:moreKeys="&#x090B;,&#x0930;&#x093C;,&#x0960;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0915: "क" DEVANAGARI LETTER KA
                  U+0915/U+093C: "क़" DEVANAGARI LETTER KA/DEVANAGARI SIGN NUKTA -->
             <Key
                 latin:keyLabel="&#x0915;"
-                latin:moreKeys="&#x0915;&#x093C;" />
+                latin:moreKeys="&#x0915;&#x093C;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0924: "त" DEVANAGARI LETTER TA
                  U+0924/U+094D/U+0930: "त्र" DEVANAGARI LETTER TA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER RA -->
             <Key
                 latin:keyLabel="&#x0924;"
-                latin:moreKeys="&#x0924;&#x094D;&#x0930;" />
+                latin:moreKeys="&#x0924;&#x094D;&#x0930;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+091A: "च" DEVANAGARI LETTER CA -->
             <Key
-                latin:keyLabel="&#x091A;" />
+                latin:keyLabel="&#x091A;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+091F: "ट" DEVANAGARI LETTER TTA -->
             <Key
-                latin:keyLabel="&#x091F;" />
+                latin:keyLabel="&#x091F;"
+                latin:keyLabelFlags="fontNormal" />
          </default>
     </switch>
 </merge>
diff --git a/java/res/xml/rowkeys_hindi3.xml b/java/res/xml/rowkeys_hindi3.xml
index 92bcb56..ebbff3e 100644
--- a/java/res/xml/rowkeys_hindi3.xml
+++ b/java/res/xml/rowkeys_hindi3.xml
@@ -27,15 +27,18 @@
         >
             <!-- U+0911: "ऑ" DEVANAGARI LETTER CANDRA O -->
             <Key
-                latin:keyLabel="&#x0911;" />
+                latin:keyLabel="&#x0911;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0901: "ँ" DEVANAGARI SIGN CANDRABINDU
                  U+0945: "ॅ" DEVANAGARI VOWEL SIGN CANDRA E-->
             <Key
                 latin:keyLabel="&#x0901;"
-                latin:moreKeys="&#x0945;" />
+                latin:moreKeys="&#x0945;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0923: "ण" DEVANAGARI LETTER NNA -->
             <Key
-                latin:keyLabel="&#x0923;" />
+                latin:keyLabel="&#x0923;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0929: "ऩ" DEVANAGARI LETTER NNNA -->
             <Key
                 latin:keyLabel="&#x0929;" />
@@ -43,65 +46,79 @@
                  U+0934: "ऴ" DEVANAGARI LETTER LLLA -->
             <Key
                 latin:keyLabel="&#x0933;"
-                latin:moreKeys="&#x0934;" />
+                latin:moreKeys="&#x0934;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0936: "श" DEVANAGARI LETTER SHA -->
             <Key
-                latin:keyLabel="&#x0936;" />
+                latin:keyLabel="&#x0936;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0937: "ष" DEVANAGARI LETTER SSA -->
             <Key
-                latin:keyLabel="&#x0937;" />
+                latin:keyLabel="&#x0937;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0943: "ृ" DEVANAGARI VOWEL SIGN VOCALIC R
                  U+0944: "ॄ" DEVANAGARI VOWEL SIGN VOCALIC RR -->
             <Key
                 latin:keyLabel="&#x0943;"
-                latin:moreKeys="&#x0944;" />
+                latin:moreKeys="&#x0944;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+091E: "ञ" DEVANAGARI LETTER NYA -->
             <Key
-                latin:keyLabel="&#x091E;" />
+                latin:keyLabel="&#x091E;"
+                latin:keyLabelFlags="fontNormal" />
         </case>
         <default>
             <!-- U+0949: "ॉ" DEVANAGARI VOWEL SIGN CANDRA O -->
             <Key
-                latin:keyLabel="&#x0949;" />
+                latin:keyLabel="&#x0949;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0902: "ं" DEVANAGARI SIGN ANUSVARA -->
             <Key
-                latin:keyLabel="&#x0902;" />
+                latin:keyLabel="&#x0902;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+092E: "म" DEVANAGARI LETTER MA
                  U+0950: "ॐ" DEVANAGARI OM -->
             <Key
                 latin:keyLabel="&#x092E;"
-                latin:moreKeys="&#x0950;" />
+                latin:moreKeys="&#x0950;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0928: "न" DEVANAGARI LETTER NA
                  U+091E: "ञ" DEVANAGARI LETTER NYA
                  U+0919: "ङ" DEVANAGARI LETTER NGA
                  U+0928/U+093C: "ऩ" DEVANAGARI LETTER NA/DEVANAGARI SIGN NUKTA -->
             <Key
                 latin:keyLabel="&#x0928;"
-                latin:moreKeys="&#x091E;,&#x0919;,&#x0928;&#x093C;" />
+                latin:moreKeys="&#x091E;,&#x0919;,&#x0928;&#x093C;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0935: "व" DEVANAGARI LETTER VA -->
             <Key
-                latin:keyLabel="&#x0935;" />
+                latin:keyLabel="&#x0935;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0932: "ल" DEVANAGARI LETTER LA
                  U+090C: "ऌ" DEVANAGARI LETTER VOCALIC L
                  U+0961: "ॡ" DEVANAGARI LETTER VOCALIC LL -->
             <Key
                 latin:keyLabel="&#x0932;"
-                latin:moreKeys="&#x090C;,&#x0961;" />
+                latin:moreKeys="&#x090C;,&#x0961;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0938: "स" DEVANAGARI LETTER SA -->
             <Key
-                latin:keyLabel="&#x0938;" />
+                latin:keyLabel="&#x0938;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+092F: "य" DEVANAGARI LETTER YA
                  U+095F: "य़" DEVANAGARI LETTER YYA -->
             <Key
                 latin:keyLabel="&#x092F;"
-                latin:moreKeys="&#x095F;" />
+                latin:moreKeys="&#x095F;"
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+093C: "़" DEVANAGARI SIGN NUKTA
                  U+097D: "ॽ" DEVANAGARI LETTER GLOTTAL STOP
                  U+0970: "॰" DEVANAGARI ABBREVIATION SIGN
                  U+093D: "ऽ" DEVANAGARI SIGN AVAGRAHA -->
             <Key
                 latin:keyLabel="&#x093C;"
-                latin:moreKeys="&#x097D;,&#x0970;,&#x093D;" />
+                latin:moreKeys="&#x097D;,&#x0970;,&#x093D;"
+                latin:keyLabelFlags="fontNormal" />
          </default>
     </switch>
 </merge>
diff --git a/java/res/xml/rowkeys_qwerty1.xml b/java/res/xml/rowkeys_qwerty1.xml
index 84d6134..e7c9b59 100644
--- a/java/res/xml/rowkeys_qwerty1.xml
+++ b/java/res/xml/rowkeys_qwerty1.xml
@@ -22,11 +22,12 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <Key
-        latin:keyLabel="q"
+        latin:keyLabel="!text/keylabel_for_q"
         latin:keyHintLabel="1"
-        latin:additionalMoreKeys="1" />
+        latin:additionalMoreKeys="1"
+        latin:moreKeys="!text/more_keys_for_q" />
     <Key
-        latin:keyLabel="w"
+        latin:keyLabel="!text/keylabel_for_w"
         latin:keyHintLabel="2"
         latin:additionalMoreKeys="2"
         latin:moreKeys="!text/more_keys_for_w" />
@@ -46,7 +47,7 @@
         latin:additionalMoreKeys="5"
         latin:moreKeys="!text/more_keys_for_t" />
     <Key
-        latin:keyLabel="y"
+        latin:keyLabel="!text/keylabel_for_y"
         latin:keyHintLabel="6"
         latin:additionalMoreKeys="6"
         latin:moreKeys="!text/more_keys_for_y" />
diff --git a/java/res/xml/rowkeys_qwerty3.xml b/java/res/xml/rowkeys_qwerty3.xml
index a74aeb8..b70fd72 100644
--- a/java/res/xml/rowkeys_qwerty3.xml
+++ b/java/res/xml/rowkeys_qwerty3.xml
@@ -25,7 +25,8 @@
         latin:keyLabel="z"
         latin:moreKeys="!text/more_keys_for_z" />
     <Key
-        latin:keyLabel="x" />
+        latin:keyLabel="!text/keylabel_for_x"
+        latin:moreKeys="!text/more_keys_for_x" />
     <Key
         latin:keyLabel="c"
         latin:moreKeys="!text/more_keys_for_c" />
diff --git a/java/res/xml/rowkeys_spanish2.xml b/java/res/xml/rowkeys_spanish2.xml
index 4c7e579..335dff3 100644
--- a/java/res/xml/rowkeys_spanish2.xml
+++ b/java/res/xml/rowkeys_spanish2.xml
@@ -25,5 +25,5 @@
         latin:keyboardLayout="@xml/rowkeys_qwerty2" />
     <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE -->
     <Key
-        latin:keyLabel="&#x00F1;" />
+        latin:keyLabel="!text/keylabel_for_spanish_row2_10" />
  </merge>
diff --git a/java/res/xml/rowkeys_symbols3.xml b/java/res/xml/rowkeys_symbols3.xml
index c89716b..7722ca9 100644
--- a/java/res/xml/rowkeys_symbols3.xml
+++ b/java/res/xml/rowkeys_symbols3.xml
@@ -22,7 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <Key
-        latin:keyLabel="!text/keylabel_for_symbols_exclamation"
+        latin:keyLabel="!"
         latin:moreKeys="!text/more_keys_for_symbols_exclamation" />
     <switch>
         <case
diff --git a/java/res/xml/rowkeys_thai1.xml b/java/res/xml/rowkeys_thai1.xml
index 4b49da1..950d2a4 100644
--- a/java/res/xml/rowkeys_thai1.xml
+++ b/java/res/xml/rowkeys_thai1.xml
@@ -25,100 +25,110 @@
         <case
             latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
         >
-            <!-- U+0E0E: "ฎ" THAI CHARACTER DO CHADA -->
             <Key
-                latin:keyLabel="&#x0E0E;" />
-            <!-- U+0E11: "ฑ" THAI CHARACTER THO NANGMONTHO -->
-            <Key
-                latin:keyLabel="&#x0E11;" />
-            <!-- U+0E18: "ธ" THAI CHARACTER THO THONG -->
-            <Key
-                latin:keyLabel="&#x0E18;" />
-            <!-- U+0E13: "ณ" THAI CHARACTER NO NEN -->
-            <Key
-                latin:keyLabel="&#x0E13;" />
-            <!-- U+0E0D: "ญ" THAI CHARACTER YO YING -->
-            <Key
-                latin:keyLabel="&#x0E0D;" />
-            <!-- U+0E10: "ฐ" THAI CHARACTER THO THAN -->
-            <Key
-                latin:keyLabel="&#x0E10;" />
-            <!-- U+0E03: "ฃ" THAI CHARACTER KHO KHUAT -->
-            <Key
-                latin:keyLabel="&#x0E03;" />
-            <!-- U+0E05: "ฅ" THAI CHARACTER KHO KHON -->
-            <Key
-                latin:keyLabel="&#x0E05;" />
-            <!-- U+0E51: "๑" THAI DIGIT ONE
-                 U+0E52: "๒" THAI DIGIT TWO
-                 U+0E53: "๓" THAI DIGIT THREE
-                 U+0E54: "๔" THAI DIGIT FOUR
-                 U+0E55: "๕" THAI DIGIT FIVE -->
+                latin:keyLabel="+" />
+            <!-- U+0E51: "๑" THAI DIGIT ONE -->
             <Key
                 latin:keyLabel="&#x0E51;"
-                latin:moreKeys="!fixedColumnOrder!4,&#x0E52;,&#x0E53;,&#x0E54;,&#x0E55;" />
-            <!-- U+0E56: "๖" THAI DIGIT SIX
-                 U+0E57: "๗" THAI DIGIT SEVEN
-                 U+0E58: "๘" THAI DIGIT EIGHT
-                 U+0E59: "๙" THAI DIGIT NINE
-                 U+0E50: "๐" THAI DIGIT ZERO -->
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E52: "๒" THAI DIGIT TWO -->
+            <Key
+                latin:keyLabel="&#x0E52;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E53: "๓" THAI DIGIT THREE -->
+            <Key
+                latin:keyLabel="&#x0E53;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E54: "๔" THAI DIGIT FOUR -->
+            <Key
+                latin:keyLabel="&#x0E54;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E39: " ู" THAI CHARACTER SARA UU -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E39;"
+                latin:code="0x0E39"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT -->
+            <Key
+                latin:keyLabel="&#x0E3F;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E55: "๕" THAI DIGIT FIVE -->
+            <Key
+                latin:keyLabel="&#x0E55;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E56: "๖" THAI DIGIT SIX -->
             <Key
                 latin:keyLabel="&#x0E56;"
-                latin:moreKeys="!fixedColumnOrder!4,&#x0E57;,&#x0E58;,&#x0E59;,&#x0E50;" />
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E57: "๗" THAI DIGIT SEVEN -->
+            <Key
+                latin:keyLabel="&#x0E57;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E58: "๘" THAI DIGIT EIGHT -->
+            <Key
+                latin:keyLabel="&#x0E58;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E59: "๙" THAI DIGIT NINE -->
+            <Key
+                latin:keyLabel="&#x0E59;"
+                latin:keyLabelFlags="fontNormal" />
         </case>
         <default>
+            <!-- U+0E45: "ๅ" THAI CHARACTER LAKKHANGYAO -->
+            <Key
+                latin:keyLabel="&#x0E45;"
+                latin:keyLabelFlags="fontNormal" />
+            <Key
+                latin:keyLabel="/" />
+            <Key
+                latin:keyLabel="_" />
             <!-- U+0E20: "ภ" THAI CHARACTER PHO SAMPHAO -->
             <Key
                 latin:keyLabel="&#x0E20;"
-                latin:keyHintLabel="1"
-                latin:additionalMoreKeys="1,&#x0E51;" />
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0E16: "ถ" THAI CHARACTER THO THUNG -->
             <Key
                 latin:keyLabel="&#x0E16;"
-                latin:keyHintLabel="2"
-                latin:additionalMoreKeys="2,&#x0E52;" />
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E38: " ุ" THAI CHARACTER SARA U -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E38;"
+                latin:code="0x0E38"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0020: " " SPACE
+                 U+0E36: " ึ" THAI CHARACTER SARA UE -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E36;"
+                latin:code="0x0E36"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
             <!-- U+0E04: "ค" THAI CHARACTER KHO KHWAI -->
             <Key
                 latin:keyLabel="&#x0E04;"
-                latin:keyHintLabel="3"
-                latin:additionalMoreKeys="3,&#x0E53;" />
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0E15: "ต" THAI CHARACTER TO TAO -->
             <Key
                 latin:keyLabel="&#x0E15;"
-                latin:keyHintLabel="4"
-                latin:additionalMoreKeys="4,&#x0E54;" />
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0E08: "จ" THAI CHARACTER CHO CHAN -->
             <Key
                 latin:keyLabel="&#x0E08;"
-                latin:keyHintLabel="5"
-                latin:additionalMoreKeys="5,&#x0E55;" />
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0E02: "ข" THAI CHARACTER KHO KHAI -->
             <Key
                 latin:keyLabel="&#x0E02;"
-                latin:keyHintLabel="6"
-                latin:additionalMoreKeys="6,&#x0E56;" />
+                latin:keyLabelFlags="fontNormal" />
             <!-- U+0E0A: "ช" THAI CHARACTER CHO CHANG -->
             <Key
                 latin:keyLabel="&#x0E0A;"
-                latin:keyHintLabel="7"
-                latin:additionalMoreKeys="7,&#x0E57;" />
-            <!-- U+0E23: "ร" THAI CHARACTER RO RUA
-                 U+0E25: "ล" THAI CHARACTER LO LING -->
-            <Key
-                latin:keyLabel="&#x0E23;"
-                latin:moreKeys="&#x0E25;"
-                latin:keyHintLabel="8"
-                latin:additionalMoreKeys="8,&#x0E58;" />
-            <!-- U+0E19: "น" THAI CHARACTER NO NU -->
-            <Key
-                latin:keyLabel="&#x0E19;"
-                latin:keyHintLabel="9"
-                latin:additionalMoreKeys="9,&#x0E59;" />
-            <!-- U+0E22: "ย" THAI CHARACTER YO YAK -->
-            <Key
-                latin:keyLabel="&#x0E22;"
-                latin:keyHintLabel="0"
-                latin:additionalMoreKeys="0,&#x0E50;" />
+                latin:keyLabelFlags="fontNormal" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/rowkeys_thai2.xml b/java/res/xml/rowkeys_thai2.xml
index 80e3563..f602994 100644
--- a/java/res/xml/rowkeys_thai2.xml
+++ b/java/res/xml/rowkeys_thai2.xml
@@ -25,83 +25,116 @@
         <case
             latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
         >
-            <!-- U+0E24: "ฤ" THAI CHARACTER RU -->
+            <!-- U+0E50: "๐" THAI DIGIT ZERO -->
             <Key
-                latin:keyLabel="&#x0E24;" />
-            <!-- U+0E06: "ฆ" THAI CHARACTER KHO RAKHANG -->
+                latin:keyLabel="&#x0E50;"
+                latin:keyLabelFlags="fontNormal" />
             <Key
-                latin:keyLabel="&#x0E06;" />
-            <!-- U+0E0F: "ฏ" THAI CHARACTER TO PATAK -->
+                latin:keyLabel="&quot;" />
+            <!-- U+0E0E: "ฎ" THAI CHARACTER DO CHADA -->
             <Key
-                latin:keyLabel="&#x0E0F;" />
-            <!-- U+0E0C: "ฌ" THAI CHARACTER CHO CHOE -->
+                latin:keyLabel="&#x0E0E;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E11: "ฑ" THAI CHARACTER THO NANGMONTHO -->
             <Key
-                latin:keyLabel="&#x0E0C;" />
-            <!-- U+0E29: "ษ" THAI CHARACTER SO RUSI -->
+                latin:keyLabel="&#x0E11;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E18: "ธ" THAI CHARACTER THO THONG -->
             <Key
-                latin:keyLabel="&#x0E29;" />
-            <!-- U+0E28: "ศ" THAI CHARACTER SO SALA -->
+                latin:keyLabel="&#x0E18;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E4D: " ํ" THAI CHARACTER THANTHAKHAT -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
             <Key
-                latin:keyLabel="&#x0E28;" />
-            <!-- U+0E0B: "ซ" THAI CHARACTER SO SO -->
+                latin:keyLabel="&#x20;&#x0E4D;"
+                latin:code="0x0E4D"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0020: " " SPACE
+                 U+0E4A: " ๊" THAI CHARACTER MAI TRI -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
             <Key
-                latin:keyLabel="&#x0E0B;" />
-            <!-- U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT
-                 U+0E45: "ๅ" THAI CHARACTER LAKKHANGYAO -->
+                latin:keyLabel="&#x20;&#x0E4A;"
+                latin:code="0x0E4A"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0E13: "ณ" THAI CHARACTER NO NEN -->
             <Key
-                latin:keyLabel="&#x0E3F;"
-                latin:moreKeys="&#x0E45;" />
-            <!-- U+0E46: "ๆ" THAI CHARACTER MAIYAMOK
-                 U+0E2F: "ฯ" THAI CHARACTER PAIYANNOI -->
+                latin:keyLabel="&#x0E13;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E2F: "ฯ" THAI CHARACTER PAIYANNOI -->
             <Key
-                latin:keyLabel="&#x0E46;"
-                latin:moreKeys="&#x0E2F;" />
+                latin:keyLabel="&#x0E2F;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E0D: "ญ" THAI CHARACTER YO YING -->
+            <Key
+                latin:keyLabel="&#x0E0D;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E10: "ฐ" THAI CHARACTER THO THAN -->
+            <Key
+                latin:keyLabel="&#x0E10;"
+                latin:keyLabelFlags="fontNormal" />
+            <Key
+                latin:keyLabel="," />
         </case>
         <default>
-            <!-- U+0E1F: "ฟ" THAI CHARACTER FO FAN
-                 U+0E1E: "พ" THAI CHARACTER PHO PHAN -->
+            <!-- U+0E46: "ๆ" THAI CHARACTER MAIYAMOK -->
             <Key
-                latin:keyLabel="&#x0E1F;"
-                latin:moreKeys="&#x0E1E;" />
-            <!-- U+0E2B: "ห" THAI CHARACTER HO HIP -->
+                latin:keyLabel="&#x0E46;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E44: "ไ" THAI CHARACTER SARA AI MAIMALAI -->
             <Key
-                latin:keyLabel="&#x0E2B;" />
-            <!-- U+0E01: "ก" THAI CHARACTER KO KAI -->
+                latin:keyLabel="&#x0E44;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E33: "ำ" THAI CHARACTER SARA AM -->
             <Key
-                latin:keyLabel="&#x0E01;" />
-            <!-- U+0E14: "ด" THAI CHARACTER DO DEK -->
+                latin:keyLabel="&#x0E33;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E1E: "พ" THAI CHARACTER PHO PHAN -->
             <Key
-                latin:keyLabel="&#x0E14;" />
-            <!-- U+0E2A: "ส" THAI CHARACTER SO SUA -->
-            <Key
-                latin:keyLabel="&#x0E2A;" />
-            <!-- U+0E27: "ว" THAI CHARACTER WO WAEN -->
-            <Key
-                latin:keyLabel="&#x0E27;" />
-            <!-- U+0E07: "ง" THAI CHARACTER NGO NGU -->
-            <Key
-                latin:keyLabel="&#x0E07;" />
-            <!-- U+0E30: "ะ" THAI CHARACTER SARA A
-                 U+0E32: "า" THAI CHARACTER SARA AA
-                 U+0E33: " ำ" THAI CHARACTER SARA AM
-                 U+0E40: "เ" THAI CHARACTER SARA E
-                 U+0E41: "แ" THAI CHARACTER SARA AE
-                 U+0E43: "ใ" THAI CHARACTER SARA AI MAIMUAN
-                 U+0E44: "ไ" THAI CHARACTER SARA AI MAIMALAI
-                 U+0E42: "โ" THAI CHARACTER SARA O -->
+                latin:keyLabel="&#x0E1E;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E30: "ะ" THAI CHARACTER SARA A -->
             <Key
                 latin:keyLabel="&#x0E30;"
-                latin:moreKeys="&#x0E32;,&#x0E33;,&#x0E40;,&#x0E41;,&#x0E43;,&#x0E44;,&#x0E42;" />
-            <!-- U+0E31: " ั" THAI CHARACTER MAI HAN-AKAT
-                 U+0E34: " ิ" THAI CHARACTER SARA I
-                 U+0E35: " ี" THAI CHARACTER SARA II
-                 U+0E36: " ึ" THAI CHARACTER SARA UE
-                 U+0E37: " ื" THAI CHARACTER SARA UEE
-                 U+0E38: " ุ" THAI CHARACTER SARA U
-                 U+0E39: " ู" THAI CHARACTER SARA UU -->
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E31: " ั" THAI CHARACTER MAI HAN-AKAT -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
             <Key
-                latin:keyLabel="&#x0E31;"
-                latin:moreKeys="&#x0E34;,&#x0E35;,&#x0E36;,&#x0E37;,&#x0E38;,&#x0E39;" />
+                latin:keyLabel="&#x20;&#x0E31;"
+                latin:code="0x0E31"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0020: " " SPACE
+                 U+0E35: " ี" HAI CHARACTER SARA II -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E35;"
+                latin:code="0x0E35"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0E23: "ร" THAI CHARACTER RO RUA -->
+            <Key
+                latin:keyLabel="&#x0E23;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E19: "น" THAI CHARACTER NO NU -->
+            <Key
+                latin:keyLabel="&#x0E19;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E22: "ย" THAI CHARACTER YO YAK -->
+            <Key
+                latin:keyLabel="&#x0E22;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E1A: "บ" THAI CHARACTER BO BAIMAI -->
+            <Key
+                latin:keyLabel="&#x0E1A;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E25: "ล" THAI CHARACTER LO LING -->
+            <Key
+                latin:keyLabel="&#x0E25;"
+                latin:keyLabelFlags="fontNormal" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/rowkeys_thai3.xml b/java/res/xml/rowkeys_thai3.xml
index b833807..7b6e637 100644
--- a/java/res/xml/rowkeys_thai3.xml
+++ b/java/res/xml/rowkeys_thai3.xml
@@ -25,59 +25,110 @@
         <case
             latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
         >
-            <!-- U+0E09: "ฉ" THAI CHARACTER CHO CHING -->
+            <!-- U+0E24: "ฤ" THAI CHARACTER RU -->
             <Key
-                latin:keyLabel="&#x0E09;" />
-            <!-- U+0E2E: "ฮ" THAI CHARACTER HO NOKHUK -->
+                latin:keyLabel="&#x0E24;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E06: "ฆ" THAI CHARACTER KHO RAKHANG -->
             <Key
-                latin:keyLabel="&#x0E2E;" />
-            <!-- U+0E12: "ฒ" THAI CHARACTER THO PHUTHAO -->
+                latin:keyLabel="&#x0E06;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E0F: "ฏ" THAI CHARACTER TO PATAK -->
             <Key
-                latin:keyLabel="&#x0E12;" />
-            <!-- U+0E2C: "ฬ" THAI CHARACTER LO CHULA -->
+                latin:keyLabel="&#x0E0F;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E42: "โ" THAI CHARACTER SARA O -->
             <Key
-                latin:keyLabel="&#x0E2C;" />
-            <!-- U+0E26: "ฦ" THAI CHARACTER LU -->
+                latin:keyLabel="&#x0E42;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E0C: "ฌ" THAI CHARACTER CHO CHOE -->
             <Key
-                latin:keyLabel="&#x0E26;" />
-            <!-- U+0E4C: " ์" THAI CHARACTER THANTHAKHAT
-                 U+0E4D: " ํ" THAI CHARACTER NIKHAHIT
-                 U+0E3A: " ฺ" THAI CHARACTER PHINTHU -->
+                latin:keyLabel="&#x0E0C;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E47: " ็" THAI CHARACTER MAITAIKHU -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
             <Key
-                latin:keyLabel="&#x0E4C;"
-                latin:moreKeys="&#x0E4D;,&#x0E3A;" />
-            <!-- U+0E47: " ็" THAI CHARACTER MAITAIKHU -->
+                latin:keyLabel="&#x20;&#x0E47;"
+                latin:code="0x0E47"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0020: " " SPACE
+                 U+0E4B: " ๋" THAI CHARACTER MAI CHATTAWA -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
             <Key
-                latin:keyLabel="&#x0E47;" />
+                latin:keyLabel="&#x20;&#x0E4B;"
+                latin:code="0x0E4B"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0E29: "ษ" THAI CHARACTER SO RUSI -->
+            <Key
+                latin:keyLabel="&#x0E29;"
+                latin:keyLabelFlags="fontNormal" />
+            <!--  U+0E28: "ศ" THAI CHARACTER SO SALA -->
+            <Key
+                latin:keyLabel="&#x0E28;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E0B: "ซ" THAI CHARACTER SO SO -->
+            <Key
+                latin:keyLabel="&#x0E0B;"
+                latin:keyLabelFlags="fontNormal" />
+            <Key
+                latin:keyLabel="." />
         </case>
         <default>
-            <!-- U+0E1C: "ผ" THAI CHARACTER PHO PHUNG -->
+            <!-- U+0E1F: "ฟ" THAI CHARACTER FO FAN -->
             <Key
-                latin:keyLabel="&#x0E1C;" />
-            <!-- U+0E1B: "ป" THAI CHARACTER PO PLA
-                 U+0E1A: "บ" THAI CHARACTER BO BAIMAI -->
+                latin:keyLabel="&#x0E1F;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E2B: "ห" THAI CHARACTER HO HIP -->
             <Key
-                latin:keyLabel="&#x0E1B;"
-                latin:moreKeys="&#x0E1A;" />
-            <!-- U+0E2D: "อ" THAI CHARACTER O ANG -->
+                latin:keyLabel="&#x0E2B;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E01: "ก" THAI CHARACTER KO KAI -->
             <Key
-                latin:keyLabel="&#x0E2D;" />
-            <!-- U+0E17: "ท" THAI CHARACTER THO THAHAN -->
+                latin:keyLabel="&#x0E01;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E14: "ด" THAI CHARACTER DO DEK -->
             <Key
-                latin:keyLabel="&#x0E17;" />
-            <!-- U+0E21: "ม" THAI CHARACTER MO MA -->
+                latin:keyLabel="&#x0E14;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E40: "เ" THAI CHARACTER SARA E -->
             <Key
-                latin:keyLabel="&#x0E21;" />
-            <!-- U+0E1D: "ฝ" THAI CHARACTER FO FA -->
+                latin:keyLabel="&#x0E40;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E49: " ้" THAI CHARACTER MAI THO -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
             <Key
-                latin:keyLabel="&#x0E1D;" />
-            <!-- U+0E48: " ่" THAI CHARACTER MAI EK
-                 U+0E49: " ้" THAI CHARACTER MAI THO
-                 U+0E4A: " ๊" THAI CHARACTER MAI TRI
-                 U+0E4B: " ๋" THAI CHARACTER MAI CHATTAWA -->
+                latin:keyLabel="&#x20;&#x0E49;"
+                latin:code="0x0E49"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0020: " " SPACE
+                 U+0E48: " ่" THAI CHARACTER MAI EK -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
             <Key
-                latin:keyLabel="&#x0E48;"
-                latin:moreKeys="&#x0E49;,&#x0E4A;,&#x0E4B;" />
+                latin:keyLabel="&#x20;&#x0E48;"
+                latin:code="0x0E48"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0E32: "า" THAI CHARACTER SARA AA -->
+            <Key
+                latin:keyLabel="&#x0E32;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E2A: "ส" THAI CHARACTER SO SUA -->
+            <Key
+                latin:keyLabel="&#x0E2A;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E27: "ว" THAI CHARACTER WO WAEN -->
+            <Key
+                latin:keyLabel="&#x0E27;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E07: "ง" THAI CHARACTER NGO NGU -->
+            <Key
+                latin:keyLabel="&#x0E07;"
+                latin:keyLabelFlags="fontNormal" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/rowkeys_thai4.xml b/java/res/xml/rowkeys_thai4.xml
new file mode 100644
index 0000000..8a78424
--- /dev/null
+++ b/java/res/xml/rowkeys_thai4.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <switch>
+        <case
+            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
+        >
+            <Key
+                latin:keyLabel="(" />
+            <Key
+                latin:keyLabel=")" />
+            <!-- U+0E09: "ฉ" THAI CHARACTER CHO CHING -->
+            <Key
+                latin:keyLabel="&#x0E09;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E2E: "ฮ" THAI CHARACTER HO NOKHUK -->
+            <Key
+                latin:keyLabel="&#x0E2E;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E3A: " ฺ" THAI CHARACTER PHINTHU -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E3A;"
+                latin:code="0x0E3A"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0020: " " SPACE
+                 U+0E4C: " ์" THAI CHARACTER THANTHAKHAT -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E4C;"
+                latin:code="0x0E4C"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <Key
+                latin:keyLabel="\?" />
+            <!-- U+0E12: "ฒ" THAI CHARACTER THO PHUTHAO -->
+            <Key
+                latin:keyLabel="&#x0E12;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E2C: "ฬ" THAI CHARACTER LO CHULA -->
+            <Key
+                latin:keyLabel="&#x0E2C;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E26: "ฦ" THAI CHARACTER LU -->
+            <Key
+                latin:keyLabel="&#x0E26;"
+                latin:keyLabelFlags="fontNormal" />
+        </case>
+        <default>
+            <!-- U+0E1C: "ผ" THAI CHARACTER PHO PHUNG -->
+            <Key
+                latin:keyLabel="&#x0E1C;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E1B: "ป" THAI CHARACTER PO PLA -->
+            <Key
+                latin:keyLabel="&#x0E1B;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E41: "แ" THAI CHARACTER SARA AE -->
+            <Key
+                latin:keyLabel="&#x0E41;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E2D: "อ" THAI CHARACTER O ANG -->
+            <Key
+                latin:keyLabel="&#x0E2D;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0020: " " SPACE
+                 U+0E34: " ิ" THAI CHARACTER SARA I -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E34;"
+                latin:code="0x0E34"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0020: " " SPACE
+                 U+0E37: " ื" THAI CHARACTER SARA UEE -->
+            <!-- Note: The space character is needed as a preceding letter to draw some Thai
+                 composing characters correctly. -->
+            <Key
+                latin:keyLabel="&#x20;&#x0E37;"
+                latin:code="0x0E37"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0E17: "ท" THAI CHARACTER THO THAHAN -->
+            <Key
+                latin:keyLabel="&#x0E17;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E21: "ม" THAI CHARACTER MO MA -->
+            <Key
+                latin:keyLabel="&#x0E21;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E43: "ใ" THAI CHARACTER SARA AI MAIMUAN -->
+            <Key
+                latin:keyLabel="&#x0E43;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E1D: "ฝ" THAI CHARACTER FO FA -->
+            <Key
+                latin:keyLabel="&#x0E1D;"
+                latin:keyLabelFlags="fontNormal" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/rows_esperanto.xml b/java/res/xml/rows_esperanto.xml
deleted file mode 100644
index c5f626e..0000000
--- a/java/res/xml/rows_esperanto.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <include
-        latin:keyboardLayout="@xml/key_styles_common" />
-    <Row
-        latin:keyWidth="10%p"
-    >
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto1" />
-    </Row>
-    <Row
-        latin:keyWidth="10%p"
-    >
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto2" />
-    </Row>
-    <Row
-        latin:keyWidth="10%p"
-    >
-        <Key
-            latin:keyStyle="shiftKeyStyle"
-            latin:keyWidth="15%p"
-            latin:visualInsetsRight="1%p" />
-        <include
-            latin:keyboardLayout="@xml/rowkeys_esperanto3" />
-        <Key
-            latin:keyStyle="deleteKeyStyle"
-            latin:keyWidth="fillRight"
-            latin:visualInsetsLeft="1%p" />
-    </Row>
-    <include
-        latin:keyboardLayout="@xml/row_qwerty4" />
-</merge>
diff --git a/java/res/xml/rows_thai.xml b/java/res/xml/rows_thai.xml
index 6b80df6..108b7e1 100644
--- a/java/res/xml/rows_thai.xml
+++ b/java/res/xml/rows_thai.xml
@@ -24,31 +24,34 @@
     <include
         latin:keyboardLayout="@xml/key_styles_common" />
     <Row
-        latin:keyWidth="10%p"
+        latin:keyWidth="8.3333%p"
     >
         <include
             latin:keyboardLayout="@xml/rowkeys_thai1" />
     </Row>
     <Row
-        latin:keyWidth="10%p"
+        latin:keyWidth="8.3333%p"
     >
         <include
-            latin:keyboardLayout="@xml/rowkeys_thai2"
-            latin:keyXPos="5%p" />
+            latin:keyboardLayout="@xml/rowkeys_thai2" />
     </Row>
     <Row
-        latin:keyWidth="10%p"
+        latin:keyWidth="8.3333%p"
     >
-        <Key
-            latin:keyStyle="shiftKeyStyle"
-            latin:keyWidth="15%p"
-            latin:visualInsetsRight="1%p" />
         <include
             latin:keyboardLayout="@xml/rowkeys_thai3" />
+        <include
+            latin:keyboardLayout="@xml/key_thai_kho_khuat" />
+    </Row>
+    <Row
+        latin:keyWidth="8.3333%p"
+    >
         <Key
-            latin:keyStyle="deleteKeyStyle"
-            latin:keyWidth="fillRight"
-            latin:visualInsetsLeft="1%p" />
+            latin:keyStyle="shiftKeyStyle" />
+        <include
+            latin:keyboardLayout="@xml/rowkeys_thai4" />
+        <Key
+            latin:keyStyle="deleteKeyStyle" />
     </Row>
     <include
         latin:keyboardLayout="@xml/row_qwerty4" />
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
index 70e38fd..5af5d04 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();
@@ -195,8 +196,7 @@
             info.setSource(mKeyboardView, virtualViewId);
             info.setBoundsInScreen(boundsInScreen);
             info.setEnabled(true);
-            info.setClickable(true);
-            info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+            info.setVisibleToUser(true);
 
             if (mAccessibilityFocusedView == virtualViewId) {
                 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
@@ -225,6 +225,9 @@
 
         mKeyboardView.onTouchEvent(downEvent);
         mKeyboardView.onTouchEvent(upEvent);
+
+        downEvent.recycle();
+        upEvent.recycle();
     }
 
     @Override
@@ -251,9 +254,6 @@
         final int virtualViewId = generateVirtualViewIdForKey(key);
 
         switch (action) {
-        case AccessibilityNodeInfoCompat.ACTION_CLICK:
-            simulateKeyPress(key);
-            return true;
         case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
             if (mAccessibilityFocusedView == virtualViewId) {
                 return false;
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
index 616b1c6..1eee1df 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;
@@ -32,7 +37,7 @@
 import com.android.inputmethod.latin.InputTypeUtils;
 import com.android.inputmethod.latin.R;
 
-public class AccessibilityUtils {
+public final class AccessibilityUtils {
     private static final String TAG = AccessibilityUtils.class.getSimpleName();
     private static final String CLASS = AccessibilityUtils.class.getClass().getName();
     private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage()
@@ -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 f6376d5..01220a5 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -24,12 +24,11 @@
 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;
 import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.keyboard.LatinKeyboardView;
+import com.android.inputmethod.keyboard.MainKeyboardView;
 import com.android.inputmethod.keyboard.PointerTracker;
 import com.android.inputmethod.latin.R;
 
@@ -37,14 +36,14 @@
     private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy();
 
     private InputMethodService mInputMethod;
-    private LatinKeyboardView mView;
+    private MainKeyboardView mView;
     private AccessibilityEntityProvider mAccessibilityNodeProvider;
 
     private Key mLastHoverKey = null;
 
     /**
      * 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);
     }
 
     /**
@@ -70,7 +70,7 @@
      *
      * @param view The view to wrap.
      */
-    public void setView(LatinKeyboardView view) {
+    public void setView(MainKeyboardView view) {
         if (view == null) {
             // Ignore null views.
             return;
@@ -105,8 +105,21 @@
     }
 
     /**
-     * Receives hover events when accessibility is turned on in SDK versions ICS
-     * and higher.
+     * Intercepts touch events before dispatch when touch exploration is turned
+     * on in ICS and higher.
+     *
+     * @param event The motion event being dispatched.
+     * @return {@code true} if the event is handled
+     */
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        // To avoid accidental key presses during touch exploration, always drop
+        // touch events generated by the user.
+        return false;
+    }
+
+    /**
+     * Receives hover events when touch exploration is turned on in SDK versions
+     * ICS and higher.
      *
      * @param event The hover event.
      * @return {@code true} if the event is handled
@@ -114,8 +127,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 +142,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 +269,7 @@
             text = context.getText(R.string.spoken_description_shiftmode_off);
         }
 
-        AccessibilityUtils.getInstance().speak(text);
+        AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
     }
 
     /**
@@ -290,6 +309,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 23acb8b..5c45448 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -19,11 +19,13 @@
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.SparseIntArray;
 import android.view.inputmethod.EditorInfo;
 
 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;
@@ -37,10 +39,10 @@
     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();
 
-    // Map of key codes to spoken description resource IDs
-    private final HashMap<Integer, Integer> mKeyCodeMap;
+    // Sparse array of spoken description resource IDs indexed by key codes
+    private final SparseIntArray mKeyCodeMap;
 
     public static void init() {
         sInstance.initInternal();
@@ -51,18 +53,15 @@
     }
 
     private KeyCodeDescriptionMapper() {
-        mKeyLabelMap = new HashMap<CharSequence, Integer>();
-        mKeyCodeMap = new HashMap<Integer, Integer>();
+        mKeyCodeMap = new SparseIntArray();
     }
 
     private void initInternal() {
         // Manual label substitutions for key labels with no string resource
         mKeyLabelMap.put(":-)", R.string.spoken_description_smiley);
 
-        // Symbols that most TTS engines can't speak
-        mKeyCodeMap.put((int) ' ', R.string.spoken_description_space);
-
         // Special non-character codes defined in Keyboard
+        mKeyCodeMap.put(Keyboard.CODE_SPACE, R.string.spoken_description_space);
         mKeyCodeMap.put(Keyboard.CODE_DELETE, R.string.spoken_description_delete);
         mKeyCodeMap.put(Keyboard.CODE_ENTER, R.string.spoken_description_return);
         mKeyCodeMap.put(Keyboard.CODE_SETTINGS, R.string.spoken_description_settings);
@@ -70,6 +69,9 @@
         mKeyCodeMap.put(Keyboard.CODE_SHORTCUT, R.string.spoken_description_mic);
         mKeyCodeMap.put(Keyboard.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
         mKeyCodeMap.put(Keyboard.CODE_TAB, R.string.spoken_description_tab);
+        mKeyCodeMap.put(Keyboard.CODE_LANGUAGE_SWITCH, R.string.spoken_description_language_switch);
+        mKeyCodeMap.put(Keyboard.CODE_ACTION_NEXT, R.string.spoken_description_action_next);
+        mKeyCodeMap.put(Keyboard.CODE_ACTION_PREVIOUS, R.string.spoken_description_action_previous);
     }
 
     /**
@@ -273,7 +275,7 @@
             return context.getString(OBSCURED_KEY_RES_ID);
         }
 
-        if (mKeyCodeMap.containsKey(code)) {
+        if (mKeyCodeMap.indexOfKey(code) >= 0) {
             return context.getString(mKeyCodeMap.get(code));
         } else if (isDefinedNonCtrl) {
             return Character.toString((char) code);
diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java
index ce427e9..ffed6ec 100644
--- a/java/src/com/android/inputmethod/compat/CompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/CompatUtils.java
@@ -24,7 +24,7 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 
-public class CompatUtils {
+public final class CompatUtils {
     private static final String TAG = CompatUtils.class.getSimpleName();
     private static final String EXTRA_INPUT_METHOD_ID = "input_method_id";
     // TODO: Can these be constants instead of literal String constants?
diff --git a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
index 08c246f..210058b 100644
--- a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
@@ -20,7 +20,7 @@
 
 import java.lang.reflect.Field;
 
-public class EditorInfoCompatUtils {
+public final class EditorInfoCompatUtils {
     // EditorInfo.IME_FLAG_FORCE_ASCII has been introduced since API#16 (JellyBean).
     private static final Field FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField(
             EditorInfo.class, "IME_FLAG_FORCE_ASCII");
diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java
new file mode 100644
index 0000000..8eea31e
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java
@@ -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.
+ */
+
+package com.android.inputmethod.compat;
+
+import android.inputmethodservice.InputMethodService;
+
+import java.lang.reflect.Method;
+
+public final class InputMethodServiceCompatUtils {
+    private static final Method METHOD_enableHardwareAcceleration =
+            CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration");
+
+    private InputMethodServiceCompatUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static boolean enableHardwareAcceleration(InputMethodService ims) {
+        return (Boolean)CompatUtils.invoke(ims, false, METHOD_enableHardwareAcceleration);
+    }
+}
diff --git a/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
index 1b79992..db5abd0 100644
--- a/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
@@ -18,7 +18,7 @@
 
 import java.lang.reflect.Field;
 
-public class SettingsSecureCompatUtils {
+public final class SettingsSecureCompatUtils {
     private static final Field FIELD_ACCESSIBILITY_SPEAK_PASSWORD = CompatUtils.getField(
             android.provider.Settings.Secure.class, "ACCESSIBILITY_SPEAK_PASSWORD");
 
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
index a0f48d2..159f436 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,12 +23,17 @@
 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;
 import java.util.Locale;
 
-public class SuggestionSpanUtils {
+public final class SuggestionSpanUtils {
     private static final String TAG = SuggestionSpanUtils.class.getSimpleName();
     // TODO: Use reflection to get field values
     public static final String ACTION_SUGGESTION_PICKED =
@@ -119,8 +120,7 @@
         } else {
             spannable = new SpannableString(pickedWord);
         }
-        final ArrayList<String> suggestionsList = new ArrayList<String>();
-        boolean sameAsTyped = false;
+        final ArrayList<String> suggestionsList = CollectionUtils.newArrayList();
         for (int i = 0; i < suggestedWords.size(); ++i) {
             if (suggestionsList.size() >= OBJ_SUGGESTIONS_MAX_SIZE) {
                 break;
@@ -128,8 +128,6 @@
             final CharSequence word = suggestedWords.getWord(i);
             if (!TextUtils.equals(pickedWord, word)) {
                 suggestionsList.add(word.toString());
-            } else if (i == 0) {
-                sameAsTyped = true;
             }
         }
 
diff --git a/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
index e5f9db2..8314212 100644
--- a/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
@@ -20,7 +20,7 @@
 
 import java.lang.reflect.Field;
 
-public class SuggestionsInfoCompatUtils {
+public final class SuggestionsInfoCompatUtils {
     private static final Field FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = CompatUtils.getField(
             SuggestionsInfo.class, "RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS");
     private static final Integer OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = (Integer) CompatUtils
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index e1e1ca9..cb120a3 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -31,11 +31,16 @@
 import android.util.Log;
 import android.util.Xml;
 
+import com.android.inputmethod.keyboard.internal.KeyDrawParams;
 import com.android.inputmethod.keyboard.internal.KeySpecParser;
-import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec;
-import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle;
+import com.android.inputmethod.keyboard.internal.KeyStyle;
+import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.keyboard.internal.KeyboardParams;
+import com.android.inputmethod.keyboard.internal.KeyboardRow;
+import com.android.inputmethod.keyboard.internal.MoreKeySpec;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
 import com.android.inputmethod.latin.StringUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -47,14 +52,13 @@
 /**
  * Class for describing the position and characteristics of a single key in the keyboard.
  */
-public class Key {
+public class Key implements Comparable<Key> {
     private static final String TAG = Key.class.getSimpleName();
 
     /**
      * The key code (unicode or custom code) that this key generates.
      */
     public final int mCode;
-    public final int mAltCode;
 
     /** Label to display */
     public final String mLabel;
@@ -89,22 +93,11 @@
 
     /** Icon to display instead of a label. Icon takes precedence over a label */
     private final int mIconId;
-    /** Icon for disabled state */
-    private final int mDisabledIconId;
-    /** Preview version of the icon, for the preview popup */
-    private final int mPreviewIconId;
 
     /** Width of the key, not including the gap */
     public final int mWidth;
     /** Height of the key, not including the gap */
     public final int mHeight;
-    /** The horizontal gap around this key */
-    public final int mHorizontalGap;
-    /** The vertical gap below this key */
-    public final int mVerticalGap;
-    /** The visual insets */
-    public final int mVisualInsetsLeft;
-    public final int mVisualInsetsRight;
     /** X coordinate of the key in the keyboard layout */
     public final int mX;
     /** Y coordinate of the key in the keyboard layout */
@@ -112,8 +105,6 @@
     /** Hit bounding box of the key */
     public final Rect mHitBox = new Rect();
 
-    /** Text to output when pressed. This can be multiple characters, like ".com" */
-    public final CharSequence mOutputText;
     /** More keys */
     public final MoreKeySpec[] mMoreKeys;
     /** More keys column number and flags */
@@ -143,6 +134,34 @@
     private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
     private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
 
+    public final KeyVisualAttributes mKeyVisualAttributes;
+
+    private final OptionalAttributes mOptionalAttributes;
+
+    private static class OptionalAttributes {
+        /** Text to output when pressed. This can be multiple characters, like ".com" */
+        public final String mOutputText;
+        public final int mAltCode;
+        /** Icon for disabled state */
+        public final int mDisabledIconId;
+        /** Preview version of the icon, for the preview popup */
+        public final int mPreviewIconId;
+        /** The visual insets */
+        public final int mVisualInsetsLeft;
+        public final int mVisualInsetsRight;
+
+        public OptionalAttributes(final String outputText, final int altCode,
+                final int disabledIconId, final int previewIconId,
+                final int visualInsetsLeft, final int visualInsetsRight) {
+            mOutputText = outputText;
+            mAltCode = altCode;
+            mDisabledIconId = disabledIconId;
+            mPreviewIconId = previewIconId;
+            mVisualInsetsLeft = visualInsetsLeft;
+            mVisualInsetsRight = visualInsetsRight;
+        }
+    }
+
     private final int mHashCode;
 
     /** The current pressed state of this key */
@@ -153,8 +172,8 @@
     /**
      * This constructor is being used only for keys in more keys keyboard.
      */
-    public Key(Keyboard.Params params, MoreKeySpec moreKeySpec, int x, int y, int width, int height,
-            int labelFlags) {
+    public Key(final KeyboardParams params, final MoreKeySpec moreKeySpec, final int x, final int y,
+            final int width, final int height, final int labelFlags) {
         this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode,
                 moreKeySpec.mOutputText, x, y, width, height, labelFlags);
     }
@@ -162,13 +181,11 @@
     /**
      * This constructor is being used only for key in popup suggestions pane.
      */
-    public Key(Keyboard.Params params, String label, String hintLabel, int iconId,
-            int code, String outputText, int x, int y, int width, int height, int labelFlags) {
+    public Key(final KeyboardParams params, final String label, final String hintLabel,
+            final int iconId, final int code, final String outputText, final int x, final int y,
+            final int width, final int height, final int labelFlags) {
         mHeight = height - params.mVerticalGap;
-        mHorizontalGap = params.mHorizontalGap;
-        mVerticalGap = params.mVerticalGap;
-        mVisualInsetsLeft = mVisualInsetsRight = 0;
-        mWidth = width - mHorizontalGap;
+        mWidth = width - params.mHorizontalGap;
         mHintLabel = hintLabel;
         mLabelFlags = labelFlags;
         mBackgroundType = BACKGROUND_TYPE_NORMAL;
@@ -176,17 +193,20 @@
         mMoreKeys = null;
         mMoreKeysColumnAndFlags = 0;
         mLabel = label;
-        mOutputText = outputText;
+        if (outputText == null) {
+            mOptionalAttributes = null;
+        } else {
+            mOptionalAttributes = new OptionalAttributes(outputText, CODE_UNSPECIFIED,
+                    ICON_UNDEFINED, ICON_UNDEFINED, 0, 0);
+        }
         mCode = code;
         mEnabled = (code != CODE_UNSPECIFIED);
-        mAltCode = CODE_UNSPECIFIED;
         mIconId = iconId;
-        mDisabledIconId = ICON_UNDEFINED;
-        mPreviewIconId = ICON_UNDEFINED;
         // Horizontal gap is divided equally to both sides of the key.
-        mX = x + mHorizontalGap / 2;
+        mX = x + params.mHorizontalGap / 2;
         mY = y;
         mHitBox.set(x, y, x + width + 1, y + height);
+        mKeyVisualAttributes = null;
 
         mHashCode = computeHashCode(this);
     }
@@ -201,12 +221,11 @@
      * @param parser the XML parser containing the attributes for this key
      * @throws XmlPullParserException
      */
-    public Key(Resources res, Keyboard.Params params, Keyboard.Builder.Row row,
-            XmlPullParser parser) throws XmlPullParserException {
+    public Key(final Resources res, final KeyboardParams params, final KeyboardRow row,
+            final XmlPullParser parser) throws XmlPullParserException {
         final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
         final int keyHeight = row.mRowHeight;
-        mVerticalGap = params.mVerticalGap;
-        mHeight = keyHeight - mVerticalGap;
+        mHeight = keyHeight - params.mVerticalGap;
 
         final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
                 R.styleable.Keyboard_Key);
@@ -220,7 +239,6 @@
         mX = Math.round(keyXPos + horizontalGap / 2);
         mY = keyYPos;
         mWidth = Math.round(keyWidth - horizontalGap);
-        mHorizontalGap = Math.round(horizontalGap);
         mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
                 keyYPos + keyHeight);
         // Update row to have current x coordinate.
@@ -229,15 +247,15 @@
         mBackgroundType = style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
 
-        mVisualInsetsLeft = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr,
+        final int visualInsetsLeft = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
                 R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0));
-        mVisualInsetsRight = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr,
+        final int visualInsetsRight = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
                 R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0));
         mIconId = KeySpecParser.getIconId(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_keyIcon));
-        mDisabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
+        final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_keyIconDisabled));
-        mPreviewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
+        final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_keyIconPreview));
 
         mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
@@ -331,21 +349,28 @@
         } else {
             mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale);
         }
-        mOutputText = outputText;
-        mAltCode = KeySpecParser.toUpperCaseOfCodeForLocale(
+        final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale(
                 KeySpecParser.parseCode(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED),
                 needsToUpperCase, locale);
-        mHashCode = computeHashCode(this);
-
+        if (outputText == null && altCode == CODE_UNSPECIFIED
+                && disabledIconId == ICON_UNDEFINED && previewIconId == ICON_UNDEFINED
+                && visualInsetsLeft == 0 && visualInsetsRight == 0) {
+            mOptionalAttributes = null;
+        } else {
+            mOptionalAttributes = new OptionalAttributes(outputText, altCode,
+                    disabledIconId, previewIconId,
+                    visualInsetsLeft, visualInsetsRight);
+        }
+        mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
         keyAttr.recycle();
-
+        mHashCode = computeHashCode(this);
         if (hasShiftedLetterHint() && TextUtils.isEmpty(mHintLabel)) {
             Log.w(TAG, "hasShiftedLetterHint specified without keyHintLabel: " + this);
         }
     }
 
-    private static boolean needsToUpperCase(int labelFlags, int keyboardElementId) {
+    private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) {
         if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
         switch (keyboardElementId) {
         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
@@ -358,7 +383,7 @@
         }
     }
 
-    private static int computeHashCode(Key key) {
+    private static int computeHashCode(final Key key) {
         return Arrays.hashCode(new Object[] {
                 key.mX,
                 key.mY,
@@ -370,22 +395,22 @@
                 key.mIconId,
                 key.mBackgroundType,
                 Arrays.hashCode(key.mMoreKeys),
-                key.mOutputText,
+                key.getOutputText(),
                 key.mActionFlags,
                 key.mLabelFlags,
                 // Key can be distinguishable without the following members.
-                // key.mAltCode,
-                // key.mDisabledIconId,
-                // key.mPreviewIconId,
+                // key.mOptionalAttributes.mAltCode,
+                // key.mOptionalAttributes.mDisabledIconId,
+                // key.mOptionalAttributes.mPreviewIconId,
                 // key.mHorizontalGap,
                 // key.mVerticalGap,
-                // key.mVisualInsetLeft,
-                // key.mVisualInsetRight,
+                // key.mOptionalAttributes.mVisualInsetLeft,
+                // key.mOptionalAttributes.mVisualInsetRight,
                 // key.mMaxMoreKeysColumn,
         });
     }
 
-    private boolean equals(Key o) {
+    private boolean equalsInternal(final Key o) {
         if (this == o) return true;
         return o.mX == mX
                 && o.mY == mY
@@ -397,29 +422,42 @@
                 && o.mIconId == mIconId
                 && o.mBackgroundType == mBackgroundType
                 && Arrays.equals(o.mMoreKeys, mMoreKeys)
-                && TextUtils.equals(o.mOutputText, mOutputText)
+                && TextUtils.equals(o.getOutputText(), getOutputText())
                 && o.mActionFlags == mActionFlags
                 && o.mLabelFlags == mLabelFlags;
     }
 
     @Override
+    public int compareTo(Key o) {
+        if (equalsInternal(o)) return 0;
+        if (mHashCode > o.mHashCode) return 1;
+        return -1;
+    }
+
+    @Override
     public int hashCode() {
         return mHashCode;
     }
 
     @Override
-    public boolean equals(Object o) {
-        return o instanceof Key && equals((Key)o);
+    public boolean equals(final Object o) {
+        return o instanceof Key && equalsInternal((Key)o);
     }
 
     @Override
     public String toString() {
-        return String.format("%s/%s %d,%d %dx%d %s/%s/%s",
-                Keyboard.printableCode(mCode), mLabel, mX, mY, mWidth, mHeight, mHintLabel,
+        final String label;
+        if (StringUtils.codePointCount(mLabel) == 1 && mLabel.codePointAt(0) == mCode) {
+            label = "";
+        } else {
+            label = "/" + mLabel;
+        }
+        return String.format("%s%s %d,%d %dx%d %s/%s/%s",
+                Keyboard.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel,
                 KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType));
     }
 
-    private static String backgroundName(int backgroundType) {
+    private static String backgroundName(final int backgroundType) {
         switch (backgroundType) {
         case BACKGROUND_TYPE_NORMAL: return "normal";
         case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
@@ -430,19 +468,19 @@
         }
     }
 
-    public void markAsLeftEdge(Keyboard.Params params) {
+    public void markAsLeftEdge(final KeyboardParams params) {
         mHitBox.left = params.mHorizontalEdgesPadding;
     }
 
-    public void markAsRightEdge(Keyboard.Params params) {
+    public void markAsRightEdge(final KeyboardParams params) {
         mHitBox.right = params.mOccupiedWidth - params.mHorizontalEdgesPadding;
     }
 
-    public void markAsTopEdge(Keyboard.Params params) {
+    public void markAsTopEdge(final KeyboardParams params) {
         mHitBox.top = params.mTopPadding;
     }
 
-    public void markAsBottomEdge(Keyboard.Params params) {
+    public void markAsBottomEdge(final KeyboardParams params) {
         mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding;
     }
 
@@ -450,129 +488,169 @@
         return this instanceof Spacer;
     }
 
-    public boolean isShift() {
+    public final boolean isShift() {
         return mCode == CODE_SHIFT;
     }
 
-    public boolean isModifier() {
+    public final boolean isModifier() {
         return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
     }
 
-    public boolean isRepeatable() {
+    public final boolean isRepeatable() {
         return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
     }
 
-    public boolean noKeyPreview() {
+    public final boolean noKeyPreview() {
         return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
     }
 
-    public boolean altCodeWhileTyping() {
+    public final boolean altCodeWhileTyping() {
         return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
     }
 
-    public boolean isLongPressEnabled() {
+    public final boolean isLongPressEnabled() {
         // We need not start long press timer on the key which has activated shifted letter.
         return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
                 && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
     }
 
-    public Typeface selectTypeface(Typeface defaultTypeface) {
+    public final Typeface selectTypeface(final KeyDrawParams params) {
         // TODO: Handle "bold" here too?
         if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) {
             return Typeface.DEFAULT;
         } else if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) {
             return Typeface.MONOSPACE;
         } else {
-            return defaultTypeface;
+            return params.mTypeface;
         }
     }
 
-    public int selectTextSize(int letterSize, int largeLetterSize, int labelSize,
-            int largeLabelSize, int hintLabelSize) {
+    public final int selectTextSize(final KeyDrawParams params) {
         switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
         case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
-            return letterSize;
+            return params.mLetterSize;
         case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
-            return largeLetterSize;
+            return params.mLargeLetterSize;
         case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
-            return labelSize;
+            return params.mLabelSize;
         case LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO:
-            return largeLabelSize;
+            return params.mLargeLabelSize;
         case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
-            return hintLabelSize;
+            return params.mHintLabelSize;
         default: // No follow key ratio flag specified.
-            return StringUtils.codePointCount(mLabel) == 1 ? letterSize : labelSize;
+            return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
         }
     }
 
-    public boolean isAlignLeft() {
+    public final int selectTextColor(final KeyDrawParams params) {
+        return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
+    }
+
+    public final int selectHintTextSize(final KeyDrawParams params) {
+        if (hasHintLabel()) {
+            return params.mHintLabelSize;
+        } else if (hasShiftedLetterHint()) {
+            return params.mShiftedLetterHintSize;
+        } else {
+            return params.mHintLetterSize;
+        }
+    }
+
+    public final int selectHintTextColor(final KeyDrawParams params) {
+        if (hasHintLabel()) {
+            return params.mHintLabelColor;
+        } else if (hasShiftedLetterHint()) {
+            return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
+                    : params.mShiftedLetterHintInactivatedColor;
+        } else {
+            return params.mHintLetterColor;
+        }
+    }
+
+    public final int selectMoreKeyTextSize(final KeyDrawParams params) {
+        return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize;
+    }
+
+    public final boolean isAlignLeft() {
         return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT) != 0;
     }
 
-    public boolean isAlignRight() {
+    public final boolean isAlignRight() {
         return (mLabelFlags & LABEL_FLAGS_ALIGN_RIGHT) != 0;
     }
 
-    public boolean isAlignLeftOfCenter() {
+    public final boolean isAlignLeftOfCenter() {
         return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT_OF_CENTER) != 0;
     }
 
-    public boolean hasPopupHint() {
+    public final boolean hasPopupHint() {
         return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0;
     }
 
-    public boolean hasShiftedLetterHint() {
+    public final boolean hasShiftedLetterHint() {
         return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0;
     }
 
-    public boolean hasHintLabel() {
+    public final boolean hasHintLabel() {
         return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
     }
 
-    public boolean hasLabelWithIconLeft() {
+    public final boolean hasLabelWithIconLeft() {
         return (mLabelFlags & LABEL_FLAGS_WITH_ICON_LEFT) != 0;
     }
 
-    public boolean hasLabelWithIconRight() {
+    public final boolean hasLabelWithIconRight() {
         return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0;
     }
 
-    public boolean needsXScale() {
+    public final boolean needsXScale() {
         return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
     }
 
-    public boolean isShiftedLetterActivated() {
+    public final boolean isShiftedLetterActivated() {
         return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0;
     }
 
-    public int getMoreKeysColumn() {
+    public final int getMoreKeysColumn() {
         return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_MASK;
     }
 
-    public boolean isFixedColumnOrderMoreKeys() {
+    public final boolean isFixedColumnOrderMoreKeys() {
         return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER) != 0;
     }
 
-    public boolean hasLabelsInMoreKeys() {
+    public final boolean hasLabelsInMoreKeys() {
         return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
     }
 
-    public int getMoreKeyLabelFlags() {
+    public final int getMoreKeyLabelFlags() {
         return hasLabelsInMoreKeys()
                 ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
                 : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
     }
 
-    public boolean needsDividersInMoreKeys() {
+    public final boolean needsDividersInMoreKeys() {
         return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
     }
 
-    public boolean hasEmbeddedMoreKey() {
+    public final boolean hasEmbeddedMoreKey() {
         return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY) != 0;
     }
 
-    public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
-        final int iconId = mEnabled ? mIconId : mDisabledIconId;
+    public final String getOutputText() {
+        final OptionalAttributes attrs = mOptionalAttributes;
+        return (attrs != null) ? attrs.mOutputText : null;
+    }
+
+    public final int getAltCode() {
+        final OptionalAttributes attrs = mOptionalAttributes;
+        return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
+    }
+
+    public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
+        final OptionalAttributes attrs = mOptionalAttributes;
+        final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED;
+        final int iconId = mEnabled ? mIconId : disabledIconId;
         final Drawable icon = iconSet.getIconDrawable(iconId);
         if (icon != null) {
             icon.setAlpha(alpha);
@@ -580,10 +658,22 @@
         return icon;
     }
 
-    public Drawable getPreviewIcon(KeyboardIconsSet iconSet) {
-        return mPreviewIconId != ICON_UNDEFINED
-                ? iconSet.getIconDrawable(mPreviewIconId)
-                : iconSet.getIconDrawable(mIconId);
+    public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
+        final OptionalAttributes attrs = mOptionalAttributes;
+        final int previewIconId = (attrs != null) ? attrs.mPreviewIconId : ICON_UNDEFINED;
+        return previewIconId != ICON_UNDEFINED
+                ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId);
+    }
+
+    public final int getDrawX() {
+        final OptionalAttributes attrs = mOptionalAttributes;
+        return (attrs == null) ? mX : mX + attrs.mVisualInsetsLeft;
+    }
+
+    public final int getDrawWidth() {
+        final OptionalAttributes attrs = mOptionalAttributes;
+        return (attrs == null) ? mWidth
+                : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight;
     }
 
     /**
@@ -604,11 +694,11 @@
         mPressed = false;
     }
 
-    public boolean isEnabled() {
+    public final boolean isEnabled() {
         return mEnabled;
     }
 
-    public void setEnabled(boolean enabled) {
+    public void setEnabled(final boolean enabled) {
         mEnabled = enabled;
     }
 
@@ -618,9 +708,9 @@
      * @param y the y-coordinate of the point
      * @return whether or not the point falls on the key. If the key is attached to an edge, it
      * will assume that all points between the key and the edge are considered to be on the key.
-     * @see #markAsLeftEdge(Keyboard.Params) etc.
+     * @see #markAsLeftEdge(KeyboardParams) etc.
      */
-    public boolean isOnKey(int x, int y) {
+    public boolean isOnKey(final int x, final int y) {
         return mHitBox.contains(x, y);
     }
 
@@ -630,7 +720,7 @@
      * @param y the y-coordinate of the point
      * @return the square of the distance of the point from the nearest edge of the key
      */
-    public int squaredDistanceToEdge(int x, int y) {
+    public int squaredDistanceToEdge(final int x, final int y) {
         final int left = mX;
         final int right = left + mWidth;
         final int top = mY;
@@ -696,7 +786,7 @@
      * @return the drawable state of the key.
      * @see android.graphics.drawable.StateListDrawable#setState(int[])
      */
-    public int[] getCurrentDrawableState() {
+    public final int[] getCurrentDrawableState() {
         switch (mBackgroundType) {
         case BACKGROUND_TYPE_FUNCTIONAL:
             return mPressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL;
@@ -712,15 +802,16 @@
     }
 
     public static class Spacer extends Key {
-        public Spacer(Resources res, Keyboard.Params params, Keyboard.Builder.Row row,
-                XmlPullParser parser) throws XmlPullParserException {
+        public Spacer(final Resources res, final KeyboardParams params, final KeyboardRow row,
+                final XmlPullParser parser) throws XmlPullParserException {
             super(res, params, row, parser);
         }
 
         /**
          * This constructor is being used only for divider in more keys keyboard.
          */
-        protected Spacer(Keyboard.Params params, int x, int y, int width, int height) {
+        protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
+                final int height) {
             super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED,
                     null, x, y, width, height, 0);
         }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 13e909c..f5686dc 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -16,16 +16,15 @@
 
 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;
     private int mCorrectionX;
     private int mCorrectionY;
-    private boolean mProximityCorrectOn;
 
     /**
      * This class handles key detection.
@@ -38,8 +37,9 @@
     }
 
     public void setKeyboard(Keyboard keyboard, float correctionX, float correctionY) {
-        if (keyboard == null)
+        if (keyboard == null) {
             throw new NullPointerException();
+        }
         mCorrectionX = (int)correctionX;
         mCorrectionY = (int)correctionY;
         mKeyboard = keyboard;
@@ -53,24 +53,18 @@
         return x + mCorrectionX;
     }
 
+    // TODO: Remove vertical correction.
     public int getTouchY(int y) {
         return y + mCorrectionY;
     }
 
     public Keyboard getKeyboard() {
-        if (mKeyboard == null)
+        if (mKeyboard == null) {
             throw new IllegalStateException("keyboard isn't set");
+        }
         return mKeyboard;
     }
 
-    public void setProximityCorrectionEnabled(boolean enabled) {
-        mProximityCorrectOn = enabled;
-    }
-
-    public boolean isProximityCorrectionEnabled() {
-        return mProximityCorrectOn;
-    }
-
     public boolean alwaysAllowsSlidingInput() {
         return false;
     }
@@ -89,11 +83,17 @@
         int minDistance = Integer.MAX_VALUE;
         Key primaryKey = null;
         for (final Key key: mKeyboard.getNearestKeys(touchX, touchY)) {
-            final boolean isOnKey = key.isOnKey(touchX, touchY);
+            // An edge key always has its enlarged hitbox to respond to an event that occurred in
+            // the empty area around the key. (@see Key#markAsLeftEdge(KeyboardParams)} etc.)
+            if (!key.isOnKey(touchX, touchY)) {
+                continue;
+            }
             final int distance = key.squaredDistanceToEdge(touchX, touchY);
+            if (distance > minDistance) {
+                continue;
+            }
             // To take care of hitbox overlaps, we compare mCode here too.
-            if (primaryKey == null || distance < minDistance
-                    || (distance == minDistance && isOnKey && key.mCode > primaryKey.mCode)) {
+            if (primaryKey == null || distance < minDistance || key.mCode > primaryKey.mCode) {
                 minDistance = distance;
                 primaryKey = key;
             }
@@ -109,7 +109,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 21f175d..b7c7f41 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -16,37 +16,15 @@
 
 package com.android.inputmethod.keyboard;
 
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.util.Log;
-import android.util.TypedValue;
-import android.util.Xml;
-import android.view.InflateException;
+import android.util.SparseArray;
 
-import com.android.inputmethod.keyboard.internal.KeyStyles;
-import com.android.inputmethod.keyboard.internal.KeyboardCodesSet;
+import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
-import com.android.inputmethod.keyboard.internal.KeyboardTextsSet;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeLocale;
-import com.android.inputmethod.latin.Utils;
-import com.android.inputmethod.latin.XmlParseUtils;
+import com.android.inputmethod.keyboard.internal.KeyboardParams;
+import com.android.inputmethod.latin.CollectionUtils;
 
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Locale;
 
 /**
  * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
@@ -79,6 +57,8 @@
     public static final int CODE_DASH = '-';
     public static final int CODE_SINGLE_QUOTE = '\'';
     public static final int CODE_DOUBLE_QUOTE = '"';
+    public static final int CODE_QUESTION_MARK = '?';
+    public static final int CODE_EXCLAMATION_MARK = '!';
     // TODO: Check how this should work for right-to-left languages. It seems to stand
     // that for rtl languages, a closing parenthesis is a left parenthesis. Is this
     // managed by the font? Or is it a different char?
@@ -86,10 +66,10 @@
     public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
     public static final int CODE_CLOSING_CURLY_BRACKET = '}';
     public static final int CODE_CLOSING_ANGLE_BRACKET = '>';
-    private static final int MINIMUM_LETTER_CODE = CODE_TAB;
 
     /** Special keys code. Must be negative.
-     * These should be aligned with values/keycodes.xml
+     * These should be aligned with KeyboardCodesSet.ID_TO_NAME[],
+     * KeyboardCodesSet.DEFAULT[] and KeyboardCodesSet.RTL[]
      */
     public static final int CODE_SHIFT = -1;
     public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
@@ -101,8 +81,9 @@
     public static final int CODE_ACTION_NEXT = -8;
     public static final int CODE_ACTION_PREVIOUS = -9;
     public static final int CODE_LANGUAGE_SWITCH = -10;
+    public static final int CODE_RESEARCH = -11;
     // Code value representing the code is not specified.
-    public static final int CODE_UNSPECIFIED = -11;
+    public static final int CODE_UNSPECIFIED = -12;
 
     public final KeyboardId mId;
     public final int mThemeId;
@@ -117,6 +98,9 @@
     /** Default gap between rows */
     public final int mVerticalGap;
 
+    /** Per keyboard key visual parameters */
+    public final KeyVisualAttributes mKeyVisualAttributes;
+
     public final int mMostCommonKeyHeight;
     public final int mMostCommonKeyWidth;
 
@@ -132,12 +116,12 @@
     public final Key[] mAltCodeKeysWhileTyping;
     public final KeyboardIconsSet mIconsSet;
 
-    private final HashMap<Integer, Key> mKeyCache = new HashMap<Integer, Key>();
+    private final SparseArray<Key> mKeyCache = CollectionUtils.newSparseArray();
 
     private final ProximityInfo mProximityInfo;
     private final boolean mProximityCharsCorrectionEnabled;
 
-    public Keyboard(Params params) {
+    public Keyboard(final KeyboardParams params) {
         mId = params.mId;
         mThemeId = params.mThemeId;
         mOccupiedHeight = params.mOccupiedHeight;
@@ -146,7 +130,7 @@
         mMostCommonKeyWidth = params.mMostCommonKeyWidth;
         mMoreKeysTemplate = params.mMoreKeysTemplate;
         mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn;
-
+        mKeyVisualAttributes = params.mKeyVisualAttributes;
         mTopPadding = params.mTopPadding;
         mVerticalGap = params.mVerticalGap;
 
@@ -162,7 +146,7 @@
         mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled;
     }
 
-    public boolean hasProximityCharsCorrection(int code) {
+    public boolean hasProximityCharsCorrection(final int code) {
         if (!mProximityCharsCorrectionEnabled) {
             return false;
         }
@@ -178,27 +162,29 @@
         return mProximityInfo;
     }
 
-    public Key getKey(int code) {
+    public Key getKey(final int code) {
         if (code == CODE_UNSPECIFIED) {
             return null;
         }
-        final Integer keyCode = code;
-        if (mKeyCache.containsKey(keyCode)) {
-            return mKeyCache.get(keyCode);
-        }
-
-        for (final Key key : mKeys) {
-            if (key.mCode == code) {
-                mKeyCache.put(keyCode, key);
-                return key;
+        synchronized (mKeyCache) {
+            final int index = mKeyCache.indexOfKey(code);
+            if (index >= 0) {
+                return mKeyCache.valueAt(index);
             }
+
+            for (final Key key : mKeys) {
+                if (key.mCode == code) {
+                    mKeyCache.put(code, key);
+                    return key;
+                }
+            }
+            mKeyCache.put(code, null);
+            return null;
         }
-        mKeyCache.put(keyCode, null);
-        return null;
     }
 
-    public boolean hasKey(Key aKey) {
-        if (mKeyCache.containsKey(aKey)) {
+    public boolean hasKey(final Key aKey) {
+        if (mKeyCache.indexOfValue(aKey) >= 0) {
             return true;
         }
 
@@ -211,172 +197,13 @@
         return false;
     }
 
-    public static boolean isLetterCode(int code) {
-        return code >= MINIMUM_LETTER_CODE;
+    public static boolean isLetterCode(final int code) {
+        return code >= CODE_SPACE;
     }
 
-    public static class Params {
-        public KeyboardId mId;
-        public int mThemeId;
-
-        /** Total height and width of the keyboard, including the paddings and keys */
-        public int mOccupiedHeight;
-        public int mOccupiedWidth;
-
-        /** Base height and width of the keyboard used to calculate rows' or keys' heights and
-         *  widths
-         */
-        public int mBaseHeight;
-        public int mBaseWidth;
-
-        public int mTopPadding;
-        public int mBottomPadding;
-        public int mHorizontalEdgesPadding;
-        public int mHorizontalCenterPadding;
-
-        public int mDefaultRowHeight;
-        public int mDefaultKeyWidth;
-        public int mHorizontalGap;
-        public int mVerticalGap;
-
-        public int mMoreKeysTemplate;
-        public int mMaxMoreKeysKeyboardColumn;
-
-        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 KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
-        public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
-        public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
-        public final KeyStyles mKeyStyles = new KeyStyles(mTextsSet);
-
-        public KeyboardLayoutSet.KeysCache mKeysCache;
-
-        public int mMostCommonKeyHeight = 0;
-        public int mMostCommonKeyWidth = 0;
-
-        public boolean mProximityCharsCorrectionEnabled;
-
-        public final TouchPositionCorrection mTouchPositionCorrection =
-                new TouchPositionCorrection();
-
-        public static class TouchPositionCorrection {
-            private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3;
-
-            public boolean mEnabled;
-            public float[] mXs;
-            public float[] mYs;
-            public float[] mRadii;
-
-            public void load(String[] data) {
-                final int dataLength = data.length;
-                if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) {
-                    if (LatinImeLogger.sDBG)
-                        throw new RuntimeException(
-                                "the size of touch position correction data is invalid");
-                    return;
-                }
-
-                final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
-                mXs = new float[length];
-                mYs = new float[length];
-                mRadii = new float[length];
-                try {
-                    for (int i = 0; i < dataLength; ++i) {
-                        final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE;
-                        final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
-                        final float value = Float.parseFloat(data[i]);
-                        if (type == 0) {
-                            mXs[index] = value;
-                        } else if (type == 1) {
-                            mYs[index] = value;
-                        } else {
-                            mRadii[index] = value;
-                        }
-                    }
-                } catch (NumberFormatException e) {
-                    if (LatinImeLogger.sDBG) {
-                        throw new RuntimeException(
-                                "the number format for touch position correction data is invalid");
-                    }
-                    mXs = null;
-                    mYs = null;
-                    mRadii = null;
-                }
-            }
-
-            // TODO: Remove this method.
-            public void setEnabled(boolean enabled) {
-                mEnabled = enabled;
-            }
-
-            public boolean isValid() {
-                return mEnabled && mXs != null && mYs != null && mRadii != null
-                    && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0;
-            }
-        }
-
-        protected void clearKeys() {
-            mKeys.clear();
-            mShiftKeys.clear();
-            clearHistogram();
-        }
-
-        public void onAddKey(Key newKey) {
-            final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey;
-            final boolean zeroWidthSpacer = key.isSpacer() && key.mWidth == 0;
-            if (!zeroWidthSpacer) {
-                mKeys.add(key);
-                updateHistogram(key);
-            }
-            if (key.mCode == Keyboard.CODE_SHIFT) {
-                mShiftKeys.add(key);
-            }
-            if (key.altCodeWhileTyping()) {
-                mAltCodeKeysWhileTyping.add(key);
-            }
-        }
-
-        private int mMaxHeightCount = 0;
-        private int mMaxWidthCount = 0;
-        private final HashMap<Integer, Integer> mHeightHistogram = new HashMap<Integer, Integer>();
-        private final HashMap<Integer, Integer> mWidthHistogram = new HashMap<Integer, Integer>();
-
-        private void clearHistogram() {
-            mMostCommonKeyHeight = 0;
-            mMaxHeightCount = 0;
-            mHeightHistogram.clear();
-
-            mMaxWidthCount = 0;
-            mMostCommonKeyWidth = 0;
-            mWidthHistogram.clear();
-        }
-
-        private static int updateHistogramCounter(HashMap<Integer, Integer> histogram,
-                Integer key) {
-            final int count = (histogram.containsKey(key) ? histogram.get(key) : 0) + 1;
-            histogram.put(key, count);
-            return count;
-        }
-
-        private void updateHistogram(Key key) {
-            final Integer height = key.mHeight + key.mVerticalGap;
-            final int heightCount = updateHistogramCounter(mHeightHistogram, height);
-            if (heightCount > mMaxHeightCount) {
-                mMaxHeightCount = heightCount;
-                mMostCommonKeyHeight = height;
-            }
-
-            final Integer width = key.mWidth + key.mHorizontalGap;
-            final int widthCount = updateHistogramCounter(mWidthHistogram, width);
-            if (widthCount > mMaxWidthCount) {
-                mMaxWidthCount = widthCount;
-                mMostCommonKeyWidth = width;
-            }
-        }
+    @Override
+    public String toString() {
+        return mId.toString();
     }
 
     /**
@@ -386,14 +213,14 @@
      * @return the array of the nearest keys to the given point. If the given
      * point is out of range, then an array of size zero is returned.
      */
-    public Key[] getNearestKeys(int x, int y) {
+    public Key[] getNearestKeys(final int x, final int y) {
         // Avoid dead pixels at edges of the keyboard
         final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1));
         final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1));
         return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
     }
 
-    public static String printableCode(int code) {
+    public static String printableCode(final int code) {
         switch (code) {
         case CODE_SHIFT: return "shift";
         case CODE_SWITCH_ALPHA_SYMBOL: return "symbol";
@@ -415,937 +242,4 @@
             return String.format("'\\u%04x'", code);
         }
     }
-
-   /**
-     * Keyboard Building helper.
-     *
-     * This class parses Keyboard XML file and eventually build a Keyboard.
-     * The Keyboard XML file looks like:
-     * <pre>
-     *   &gt;!-- xml/keyboard.xml --&lt;
-     *   &gt;Keyboard keyboard_attributes*&lt;
-     *     &gt;!-- Keyboard Content --&lt;
-     *     &gt;Row row_attributes*&lt;
-     *       &gt;!-- Row Content --&lt;
-     *       &gt;Key key_attributes* /&lt;
-     *       &gt;Spacer horizontalGap="32.0dp" /&lt;
-     *       &gt;include keyboardLayout="@xml/other_keys"&lt;
-     *       ...
-     *     &gt;/Row&lt;
-     *     &gt;include keyboardLayout="@xml/other_rows"&lt;
-     *     ...
-     *   &gt;/Keyboard&lt;
-     * </pre>
-     * The XML file which is included in other file must have &gt;merge&lt; as root element,
-     * such as:
-     * <pre>
-     *   &gt;!-- xml/other_keys.xml --&lt;
-     *   &gt;merge&lt;
-     *     &gt;Key key_attributes* /&lt;
-     *     ...
-     *   &gt;/merge&lt;
-     * </pre>
-     * and
-     * <pre>
-     *   &gt;!-- xml/other_rows.xml --&lt;
-     *   &gt;merge&lt;
-     *     &gt;Row row_attributes*&lt;
-     *       &gt;Key key_attributes* /&lt;
-     *     &gt;/Row&lt;
-     *     ...
-     *   &gt;/merge&lt;
-     * </pre>
-     * You can also use switch-case-default tags to select Rows and Keys.
-     * <pre>
-     *   &gt;switch&lt;
-     *     &gt;case case_attribute*&lt;
-     *       &gt;!-- Any valid tags at switch position --&lt;
-     *     &gt;/case&lt;
-     *     ...
-     *     &gt;default&lt;
-     *       &gt;!-- Any valid tags at switch position --&lt;
-     *     &gt;/default&lt;
-     *   &gt;/switch&lt;
-     * </pre>
-     * You can declare Key style and specify styles within Key tags.
-     * <pre>
-     *     &gt;switch&lt;
-     *       &gt;case mode="email"&lt;
-     *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
-     *           keyLabel=".com"
-     *         /&lt;
-     *       &gt;/case&lt;
-     *       &gt;case mode="url"&lt;
-     *         &gt;key-style styleName="f1-key" parentStyle="modifier-key"
-     *           keyLabel="http://"
-     *         /&lt;
-     *       &gt;/case&lt;
-     *     &gt;/switch&lt;
-     *     ...
-     *     &gt;Key keyStyle="shift-key" ... /&lt;
-     * </pre>
-     */
-
-    public static class Builder<KP extends Params> {
-        private static final String BUILDER_TAG = "Keyboard.Builder";
-        private static final boolean DEBUG = false;
-
-        // Keyboard XML Tags
-        private static final String TAG_KEYBOARD = "Keyboard";
-        private static final String TAG_ROW = "Row";
-        private static final String TAG_KEY = "Key";
-        private static final String TAG_SPACER = "Spacer";
-        private static final String TAG_INCLUDE = "include";
-        private static final String TAG_MERGE = "merge";
-        private static final String TAG_SWITCH = "switch";
-        private static final String TAG_CASE = "case";
-        private static final String TAG_DEFAULT = "default";
-        public static final String TAG_KEY_STYLE = "key-style";
-
-        private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
-        private static final int DEFAULT_KEYBOARD_ROWS = 4;
-
-        protected final KP mParams;
-        protected final Context mContext;
-        protected final Resources mResources;
-        private final DisplayMetrics mDisplayMetrics;
-
-        private int mCurrentY = 0;
-        private Row mCurrentRow = null;
-        private boolean mLeftEdge;
-        private boolean mTopEdge;
-        private Key mRightEdgeKey = null;
-
-        /**
-         * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
-         * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
-         * defines.
-         */
-        public static class Row {
-            // keyWidth enum constants
-            private static final int KEYWIDTH_NOT_ENUM = 0;
-            private static final int KEYWIDTH_FILL_RIGHT = -1;
-
-            private final Params mParams;
-            /** Default width of a key in this row. */
-            private float mDefaultKeyWidth;
-            /** Default height of a key in this row. */
-            public final int mRowHeight;
-            /** Default keyLabelFlags in this row. */
-            private int mDefaultKeyLabelFlags;
-            /** Default backgroundType for this row */
-            private int mDefaultBackgroundType;
-
-            private final int mCurrentY;
-            // Will be updated by {@link Key}'s constructor.
-            private float mCurrentX;
-
-            public Row(Resources res, Params params, XmlPullParser parser, int y) {
-                mParams = params;
-                TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
-                        R.styleable.Keyboard);
-                mRowHeight = (int)Builder.getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_rowHeight,
-                        params.mBaseHeight, params.mDefaultRowHeight);
-                keyboardAttr.recycle();
-                TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
-                        R.styleable.Keyboard_Key);
-                mDefaultKeyWidth = Builder.getDimensionOrFraction(keyAttr,
-                        R.styleable.Keyboard_Key_keyWidth,
-                        params.mBaseWidth, params.mDefaultKeyWidth);
-                mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
-                        Key.BACKGROUND_TYPE_NORMAL);
-                keyAttr.recycle();
-
-                // TODO: Initialize this with <Row> attribute as backgroundType is done.
-                mDefaultKeyLabelFlags = 0;
-                mCurrentY = y;
-                mCurrentX = 0.0f;
-            }
-
-            public float getDefaultKeyWidth() {
-                return mDefaultKeyWidth;
-            }
-
-            public void setDefaultKeyWidth(float defaultKeyWidth) {
-                mDefaultKeyWidth = defaultKeyWidth;
-            }
-
-            public int getDefaultKeyLabelFlags() {
-                return mDefaultKeyLabelFlags;
-            }
-
-            public void setDefaultKeyLabelFlags(int keyLabelFlags) {
-                mDefaultKeyLabelFlags = keyLabelFlags;
-            }
-
-            public int getDefaultBackgroundType() {
-                return mDefaultBackgroundType;
-            }
-
-            public void setDefaultBackgroundType(int backgroundType) {
-                mDefaultBackgroundType = backgroundType;
-            }
-
-            public void setXPos(float keyXPos) {
-                mCurrentX = keyXPos;
-            }
-
-            public void advanceXPos(float width) {
-                mCurrentX += width;
-            }
-
-            public int getKeyY() {
-                return mCurrentY;
-            }
-
-            public float getKeyX(TypedArray keyAttr) {
-                final int widthType = Builder.getEnumValue(keyAttr,
-                        R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
-
-                final int keyboardRightEdge = mParams.mOccupiedWidth
-                        - mParams.mHorizontalEdgesPadding;
-                if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
-                    final float keyXPos = Builder.getDimensionOrFraction(keyAttr,
-                            R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
-                    if (keyXPos < 0) {
-                        // If keyXPos is negative, the actual x-coordinate will be
-                        // keyboardWidth + keyXPos.
-                        // keyXPos shouldn't be less than mCurrentX because drawable area for this
-                        // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
-                        // its left hand side.
-                        return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
-                    } else {
-                        return keyXPos + mParams.mHorizontalEdgesPadding;
-                    }
-                }
-                return mCurrentX;
-            }
-
-            public float getKeyWidth(TypedArray keyAttr) {
-                return getKeyWidth(keyAttr, mCurrentX);
-            }
-
-            public float getKeyWidth(TypedArray keyAttr, float keyXPos) {
-                final int widthType = Builder.getEnumValue(keyAttr,
-                        R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
-                switch (widthType) {
-                case KEYWIDTH_FILL_RIGHT:
-                    final int keyboardRightEdge =
-                            mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
-                    // If keyWidth is fillRight, the actual key width will be determined to fill
-                    // out the area up to the right edge of the keyboard.
-                    return keyboardRightEdge - keyXPos;
-                default: // KEYWIDTH_NOT_ENUM
-                    return Builder.getDimensionOrFraction(keyAttr,
-                            R.styleable.Keyboard_Key_keyWidth,
-                            mParams.mBaseWidth, mDefaultKeyWidth);
-                }
-            }
-        }
-
-        public Builder(Context context, KP params) {
-            mContext = context;
-            final Resources res = context.getResources();
-            mResources = res;
-            mDisplayMetrics = res.getDisplayMetrics();
-
-            mParams = params;
-
-            params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
-            params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
-        }
-
-        public void setAutoGenerate(KeyboardLayoutSet.KeysCache keysCache) {
-            mParams.mKeysCache = keysCache;
-        }
-
-        public Builder<KP> load(int xmlId, KeyboardId id) {
-            mParams.mId = id;
-            final XmlResourceParser parser = mResources.getXml(xmlId);
-            try {
-                parseKeyboard(parser);
-            } catch (XmlPullParserException e) {
-                Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
-                throw new IllegalArgumentException(e);
-            } catch (IOException e) {
-                Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
-                throw new RuntimeException(e);
-            } finally {
-                parser.close();
-            }
-            return this;
-        }
-
-        // TODO: Remove this method.
-        public void setTouchPositionCorrectionEnabled(boolean enabled) {
-            mParams.mTouchPositionCorrection.setEnabled(enabled);
-        }
-
-        public void setProximityCharsCorrectionEnabled(boolean enabled) {
-            mParams.mProximityCharsCorrectionEnabled = enabled;
-        }
-
-        public Keyboard build() {
-            return new Keyboard(mParams);
-        }
-
-        private int mIndent;
-        private static final String SPACES = "                                             ";
-
-        private static String spaces(int count) {
-            return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
-        }
-
-        private void startTag(String format, Object ... args) {
-            Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
-        }
-
-        private void endTag(String format, Object ... args) {
-            Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
-        }
-
-        private void startEndTag(String format, Object ... args) {
-            Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
-            mIndent--;
-        }
-
-        private void parseKeyboard(XmlPullParser parser)
-                throws XmlPullParserException, IOException {
-            if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
-            int event;
-            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                if (event == XmlPullParser.START_TAG) {
-                    final String tag = parser.getName();
-                    if (TAG_KEYBOARD.equals(tag)) {
-                        parseKeyboardAttributes(parser);
-                        startKeyboard();
-                        parseKeyboardContent(parser, false);
-                        break;
-                    } else {
-                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
-                    }
-                }
-            }
-        }
-
-        private void parseKeyboardAttributes(XmlPullParser parser) {
-            final int displayWidth = mDisplayMetrics.widthPixels;
-            final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
-                    Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
-                    R.style.Keyboard);
-            final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
-                    R.styleable.Keyboard_Key);
-            try {
-                final int displayHeight = mDisplayMetrics.heightPixels;
-                final String keyboardHeightString = Utils.getDeviceOverrideValue(
-                        mResources, R.array.keyboard_heights, null);
-                final float keyboardHeight;
-                if (keyboardHeightString != null) {
-                    keyboardHeight = Float.parseFloat(keyboardHeightString)
-                            * mDisplayMetrics.density;
-                } else {
-                    keyboardHeight = keyboardAttr.getDimension(
-                            R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
-                }
-                final float maxKeyboardHeight = getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
-                float minKeyboardHeight = getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
-                if (minKeyboardHeight < 0) {
-                    // Specified fraction was negative, so it should be calculated against display
-                    // width.
-                    minKeyboardHeight = -getDimensionOrFraction(keyboardAttr,
-                            R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
-                }
-                final Params params = mParams;
-                // Keyboard height will not exceed maxKeyboardHeight and will not be less than
-                // minKeyboardHeight.
-                params.mOccupiedHeight = (int)Math.max(
-                        Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
-                params.mOccupiedWidth = params.mId.mWidth;
-                params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
-                params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
-                params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
-                        mParams.mOccupiedWidth, 0);
-
-                params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
-                        - params.mHorizontalCenterPadding;
-                params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr,
-                        R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
-                        params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
-                params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
-                params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
-                params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
-                        - params.mBottomPadding + params.mVerticalGap;
-                params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_rowHeight, params.mBaseHeight,
-                        params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
-
-                params.mMoreKeysTemplate = keyboardAttr.getResourceId(
-                        R.styleable.Keyboard_moreKeysTemplate, 0);
-                params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
-                        R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
-
-                params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
-                params.mIconsSet.loadIcons(keyboardAttr);
-                final String language = params.mId.mLocale.getLanguage();
-                params.mCodesSet.setLanguage(language);
-                params.mTextsSet.setLanguage(language);
-                final RunInLocale<Void> job = new RunInLocale<Void>() {
-                    @Override
-                    protected Void job(Resources res) {
-                        params.mTextsSet.loadStringResources(mContext);
-                        return null;
-                    }
-                };
-                // Null means the current system locale.
-                final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
-                        ? null : params.mId.mLocale;
-                job.runInLocale(mResources, locale);
-
-                final int resourceId = keyboardAttr.getResourceId(
-                        R.styleable.Keyboard_touchPositionCorrectionData, 0);
-                params.mTouchPositionCorrection.setEnabled(resourceId != 0);
-                if (resourceId != 0) {
-                    final String[] data = mResources.getStringArray(resourceId);
-                    params.mTouchPositionCorrection.load(data);
-                }
-            } finally {
-                keyAttr.recycle();
-                keyboardAttr.recycle();
-            }
-        }
-
-        private void parseKeyboardContent(XmlPullParser parser, boolean skip)
-                throws XmlPullParserException, IOException {
-            int event;
-            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                if (event == XmlPullParser.START_TAG) {
-                    final String tag = parser.getName();
-                    if (TAG_ROW.equals(tag)) {
-                        Row row = parseRowAttributes(parser);
-                        if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
-                        if (!skip) {
-                            startRow(row);
-                        }
-                        parseRowContent(parser, row, skip);
-                    } else if (TAG_INCLUDE.equals(tag)) {
-                        parseIncludeKeyboardContent(parser, skip);
-                    } else if (TAG_SWITCH.equals(tag)) {
-                        parseSwitchKeyboardContent(parser, skip);
-                    } else if (TAG_KEY_STYLE.equals(tag)) {
-                        parseKeyStyle(parser, skip);
-                    } else {
-                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
-                    }
-                } else if (event == XmlPullParser.END_TAG) {
-                    final String tag = parser.getName();
-                    if (DEBUG) endTag("</%s>", tag);
-                    if (TAG_KEYBOARD.equals(tag)) {
-                        endKeyboard();
-                        break;
-                    } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
-                            || TAG_MERGE.equals(tag)) {
-                        break;
-                    } else {
-                        throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW);
-                    }
-                }
-            }
-        }
-
-        private Row parseRowAttributes(XmlPullParser parser) throws XmlPullParserException {
-            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
-                    R.styleable.Keyboard);
-            try {
-                if (a.hasValue(R.styleable.Keyboard_horizontalGap))
-                    throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
-                if (a.hasValue(R.styleable.Keyboard_verticalGap))
-                    throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
-                return new Row(mResources, mParams, parser, mCurrentY);
-            } finally {
-                a.recycle();
-            }
-        }
-
-        private void parseRowContent(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            int event;
-            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                if (event == XmlPullParser.START_TAG) {
-                    final String tag = parser.getName();
-                    if (TAG_KEY.equals(tag)) {
-                        parseKey(parser, row, skip);
-                    } else if (TAG_SPACER.equals(tag)) {
-                        parseSpacer(parser, row, skip);
-                    } else if (TAG_INCLUDE.equals(tag)) {
-                        parseIncludeRowContent(parser, row, skip);
-                    } else if (TAG_SWITCH.equals(tag)) {
-                        parseSwitchRowContent(parser, row, skip);
-                    } else if (TAG_KEY_STYLE.equals(tag)) {
-                        parseKeyStyle(parser, skip);
-                    } else {
-                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
-                    }
-                } else if (event == XmlPullParser.END_TAG) {
-                    final String tag = parser.getName();
-                    if (DEBUG) endTag("</%s>", tag);
-                    if (TAG_ROW.equals(tag)) {
-                        if (!skip) {
-                            endRow(row);
-                        }
-                        break;
-                    } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
-                            || TAG_MERGE.equals(tag)) {
-                        break;
-                    } else {
-                        throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
-                    }
-                }
-            }
-        }
-
-        private void parseKey(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            if (skip) {
-                XmlParseUtils.checkEndTag(TAG_KEY, parser);
-                if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
-            } else {
-                final Key key = new Key(mResources, mParams, row, parser);
-                if (DEBUG) {
-                    startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
-                            (key.isEnabled() ? "" : " disabled"), key,
-                            Arrays.toString(key.mMoreKeys));
-                }
-                XmlParseUtils.checkEndTag(TAG_KEY, parser);
-                endKey(key);
-            }
-        }
-
-        private void parseSpacer(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            if (skip) {
-                XmlParseUtils.checkEndTag(TAG_SPACER, parser);
-                if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
-            } else {
-                final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
-                if (DEBUG) startEndTag("<%s />", TAG_SPACER);
-                XmlParseUtils.checkEndTag(TAG_SPACER, parser);
-                endKey(spacer);
-            }
-        }
-
-        private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)
-                throws XmlPullParserException, IOException {
-            parseIncludeInternal(parser, null, skip);
-        }
-
-        private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            parseIncludeInternal(parser, row, skip);
-        }
-
-        private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            if (skip) {
-                XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
-                if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
-            } else {
-                final AttributeSet attr = Xml.asAttributeSet(parser);
-                final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
-                        R.styleable.Keyboard_Include);
-                final TypedArray keyAttr = mResources.obtainAttributes(attr,
-                        R.styleable.Keyboard_Key);
-                int keyboardLayout = 0;
-                float savedDefaultKeyWidth = 0;
-                int savedDefaultKeyLabelFlags = 0;
-                int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
-                try {
-                    XmlParseUtils.checkAttributeExists(keyboardAttr,
-                            R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
-                            TAG_INCLUDE, parser);
-                    keyboardLayout = keyboardAttr.getResourceId(
-                            R.styleable.Keyboard_Include_keyboardLayout, 0);
-                    if (row != null) {
-                        if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
-                            // Override current x coordinate.
-                            row.setXPos(row.getKeyX(keyAttr));
-                        }
-                        // TODO: Remove this if-clause and do the same as backgroundType below.
-                        savedDefaultKeyWidth = row.getDefaultKeyWidth();
-                        if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
-                            // Override default key width.
-                            row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
-                        }
-                        savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
-                        // Bitwise-or default keyLabelFlag if exists.
-                        row.setDefaultKeyLabelFlags(keyAttr.getInt(
-                                R.styleable.Keyboard_Key_keyLabelFlags, 0)
-                                | savedDefaultKeyLabelFlags);
-                        savedDefaultBackgroundType = row.getDefaultBackgroundType();
-                        // Override default backgroundType if exists.
-                        row.setDefaultBackgroundType(keyAttr.getInt(
-                                R.styleable.Keyboard_Key_backgroundType,
-                                savedDefaultBackgroundType));
-                    }
-                } finally {
-                    keyboardAttr.recycle();
-                    keyAttr.recycle();
-                }
-
-                XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
-                if (DEBUG) {
-                    startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
-                            mResources.getResourceEntryName(keyboardLayout));
-                }
-                final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
-                try {
-                    parseMerge(parserForInclude, row, skip);
-                } finally {
-                    if (row != null) {
-                        // Restore default keyWidth, keyLabelFlags, and backgroundType.
-                        row.setDefaultKeyWidth(savedDefaultKeyWidth);
-                        row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
-                        row.setDefaultBackgroundType(savedDefaultBackgroundType);
-                    }
-                    parserForInclude.close();
-                }
-            }
-        }
-
-        private void parseMerge(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            if (DEBUG) startTag("<%s>", TAG_MERGE);
-            int event;
-            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                if (event == XmlPullParser.START_TAG) {
-                    final String tag = parser.getName();
-                    if (TAG_MERGE.equals(tag)) {
-                        if (row == null) {
-                            parseKeyboardContent(parser, skip);
-                        } else {
-                            parseRowContent(parser, row, skip);
-                        }
-                        break;
-                    } else {
-                        throw new XmlParseUtils.ParseException(
-                                "Included keyboard layout must have <merge> root element", parser);
-                    }
-                }
-            }
-        }
-
-        private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)
-                throws XmlPullParserException, IOException {
-            parseSwitchInternal(parser, null, skip);
-        }
-
-        private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            parseSwitchInternal(parser, row, skip);
-        }
-
-        private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
-            boolean selected = false;
-            int event;
-            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                if (event == XmlPullParser.START_TAG) {
-                    final String tag = parser.getName();
-                    if (TAG_CASE.equals(tag)) {
-                        selected |= parseCase(parser, row, selected ? true : skip);
-                    } else if (TAG_DEFAULT.equals(tag)) {
-                        selected |= parseDefault(parser, row, selected ? true : skip);
-                    } else {
-                        throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
-                    }
-                } else if (event == XmlPullParser.END_TAG) {
-                    final String tag = parser.getName();
-                    if (TAG_SWITCH.equals(tag)) {
-                        if (DEBUG) endTag("</%s>", TAG_SWITCH);
-                        break;
-                    } else {
-                        throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
-                    }
-                }
-            }
-        }
-
-        private boolean parseCase(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            final boolean selected = parseCaseCondition(parser);
-            if (row == null) {
-                // Processing Rows.
-                parseKeyboardContent(parser, selected ? skip : true);
-            } else {
-                // Processing Keys.
-                parseRowContent(parser, row, selected ? skip : true);
-            }
-            return selected;
-        }
-
-        private boolean parseCaseCondition(XmlPullParser parser) {
-            final KeyboardId id = mParams.mId;
-            if (id == null)
-                return true;
-
-            final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
-                    R.styleable.Keyboard_Case);
-            try {
-                final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
-                        R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
-                        KeyboardId.elementIdToName(id.mElementId));
-                final boolean modeMatched = matchTypedValue(a,
-                        R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
-                final boolean navigateNextMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
-                final boolean navigatePreviousMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
-                final boolean passwordInputMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
-                final boolean clobberSettingsKeyMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
-                final boolean shortcutKeyEnabledMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
-                final boolean hasShortcutKeyMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
-                final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
-                        id.mLanguageSwitchKeyEnabled);
-                final boolean isMultiLineMatched = matchBoolean(a,
-                        R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
-                final boolean imeActionMatched = matchInteger(a,
-                        R.styleable.Keyboard_Case_imeAction, id.imeAction());
-                final boolean localeCodeMatched = matchString(a,
-                        R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
-                final boolean languageCodeMatched = matchString(a,
-                        R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
-                final boolean countryCodeMatched = matchString(a,
-                        R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
-                final boolean selected = keyboardLayoutSetElementMatched && modeMatched
-                        && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
-                        && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
-                        && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched
-                        && isMultiLineMatched && imeActionMatched && localeCodeMatched
-                        && languageCodeMatched && countryCodeMatched;
-
-                if (DEBUG) {
-                    startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
-                            textAttr(a.getString(
-                                    R.styleable.Keyboard_Case_keyboardLayoutSetElement),
-                                    "keyboardLayoutSetElement"),
-                            textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
-                            textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
-                                    "imeAction"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
-                                    "navigateNext"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
-                                    "navigatePrevious"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
-                                    "clobberSettingsKey"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
-                                    "passwordInput"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
-                                    "shortcutKeyEnabled"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
-                                    "hasShortcutKey"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
-                                    "languageSwitchKeyEnabled"),
-                            booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
-                                    "isMultiLine"),
-                            textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
-                                    "localeCode"),
-                            textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
-                                    "languageCode"),
-                            textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
-                                    "countryCode"),
-                            selected ? "" : " skipped");
-                }
-
-                return selected;
-            } finally {
-                a.recycle();
-            }
-        }
-
-        private static boolean matchInteger(TypedArray a, int index, int value) {
-            // If <case> does not have "index" attribute, that means this <case> is wild-card for
-            // the attribute.
-            return !a.hasValue(index) || a.getInt(index, 0) == value;
-        }
-
-        private static boolean matchBoolean(TypedArray a, int index, boolean value) {
-            // If <case> does not have "index" attribute, that means this <case> is wild-card for
-            // the attribute.
-            return !a.hasValue(index) || a.getBoolean(index, false) == value;
-        }
-
-        private static boolean matchString(TypedArray a, int index, String value) {
-            // If <case> does not have "index" attribute, that means this <case> is wild-card for
-            // the attribute.
-            return !a.hasValue(index)
-                    || stringArrayContains(a.getString(index).split("\\|"), value);
-        }
-
-        private static boolean matchTypedValue(TypedArray a, int index, int intValue,
-                String strValue) {
-            // 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)
-                return true;
-
-            if (isIntegerValue(v)) {
-                return intValue == a.getInt(index, 0);
-            } else if (isStringValue(v)) {
-                return stringArrayContains(a.getString(index).split("\\|"), strValue);
-            }
-            return false;
-        }
-
-        private static boolean stringArrayContains(String[] array, String value) {
-            for (final String elem : array) {
-                if (elem.equals(value))
-                    return true;
-            }
-            return false;
-        }
-
-        private boolean parseDefault(XmlPullParser parser, Row row, boolean skip)
-                throws XmlPullParserException, IOException {
-            if (DEBUG) startTag("<%s>", TAG_DEFAULT);
-            if (row == null) {
-                parseKeyboardContent(parser, skip);
-            } else {
-                parseRowContent(parser, row, skip);
-            }
-            return true;
-        }
-
-        private void parseKeyStyle(XmlPullParser parser, boolean skip)
-                throws XmlPullParserException, IOException {
-            TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
-                    R.styleable.Keyboard_KeyStyle);
-            TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
-                    R.styleable.Keyboard_Key);
-            try {
-                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" : "");
-                }
-                if (!skip)
-                    mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
-            } finally {
-                keyStyleAttr.recycle();
-                keyAttrs.recycle();
-            }
-            XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
-        }
-
-        private void startKeyboard() {
-            mCurrentY += mParams.mTopPadding;
-            mTopEdge = true;
-        }
-
-        private void startRow(Row row) {
-            addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
-            mCurrentRow = row;
-            mLeftEdge = true;
-            mRightEdgeKey = null;
-        }
-
-        private void endRow(Row row) {
-            if (mCurrentRow == null)
-                throw new InflateException("orphan end row tag");
-            if (mRightEdgeKey != null) {
-                mRightEdgeKey.markAsRightEdge(mParams);
-                mRightEdgeKey = null;
-            }
-            addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
-            mCurrentY += row.mRowHeight;
-            mCurrentRow = null;
-            mTopEdge = false;
-        }
-
-        private void endKey(Key key) {
-            mParams.onAddKey(key);
-            if (mLeftEdge) {
-                key.markAsLeftEdge(mParams);
-                mLeftEdge = false;
-            }
-            if (mTopEdge) {
-                key.markAsTopEdge(mParams);
-            }
-            mRightEdgeKey = key;
-        }
-
-        private void endKeyboard() {
-            // nothing to do here.
-        }
-
-        private void addEdgeSpace(float width, Row row) {
-            row.advanceXPos(width);
-            mLeftEdge = false;
-            mRightEdgeKey = null;
-        }
-
-        public static float getDimensionOrFraction(TypedArray a, int index, int base,
-                float defValue) {
-            final TypedValue value = a.peekValue(index);
-            if (value == null)
-                return defValue;
-            if (isFractionValue(value)) {
-                return a.getFraction(index, base, base, defValue);
-            } else if (isDimensionValue(value)) {
-                return a.getDimension(index, defValue);
-            }
-            return defValue;
-        }
-
-        public static int getEnumValue(TypedArray a, int index, int defValue) {
-            final TypedValue value = a.peekValue(index);
-            if (value == null)
-                return defValue;
-            if (isIntegerValue(value)) {
-                return a.getInt(index, defValue);
-            }
-            return defValue;
-        }
-
-        private static boolean isFractionValue(TypedValue v) {
-            return v.type == TypedValue.TYPE_FRACTION;
-        }
-
-        private static boolean isDimensionValue(TypedValue v) {
-            return v.type == TypedValue.TYPE_DIMENSION;
-        }
-
-        private static boolean isIntegerValue(TypedValue v) {
-            return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
-        }
-
-        private static boolean isStringValue(TypedValue v) {
-            return v.type == TypedValue.TYPE_STRING;
-        }
-
-        private static String textAttr(String value, String name) {
-            return value != null ? String.format(" %s=%s", name, value) : "";
-        }
-
-        private static String booleanAttr(TypedArray a, int index, String name) {
-            return a.hasValue(index)
-                    ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
-        }
-    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index 275aacf..5c8f78f 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -16,6 +16,9 @@
 
 package com.android.inputmethod.keyboard;
 
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.InputPointers;
+
 public interface KeyboardActionListener {
 
     /**
@@ -42,20 +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);
 
-    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.
      *
@@ -64,6 +63,24 @@
     public void onTextInput(CharSequence text);
 
     /**
+     * Called when user started batch input.
+     */
+    public void onStartBatchInput();
+
+    /**
+     * Sends the ongoing batch input points data.
+     * @param batchPointers the batch input points representing the user input
+     */
+    public void onUpdateBatchInput(InputPointers batchPointers);
+
+    /**
+     * Sends the final batch input points data.
+     *
+     * @param batchPointers the batch input points representing the user input
+     */
+    public void onEndBatchInput(InputPointers batchPointers);
+
+    /**
      * Called when user released a finger outside any key.
      */
     public void onCancelInput();
@@ -84,10 +101,24 @@
         @Override
         public void onTextInput(CharSequence text) {}
         @Override
+        public void onStartBatchInput() {}
+        @Override
+        public void onUpdateBatchInput(InputPointers batchPointers) {}
+        @Override
+        public void onEndBatchInput(InputPointers batchPointers) {}
+        @Override
         public void onCancelInput() {}
         @Override
         public boolean onCustomRequest(int requestCode) {
             return false;
         }
+
+        // TODO: Remove this method when the vertical correction is removed.
+        public static boolean isInvalidCoordinate(int 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/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 233716a..1e52773 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -55,10 +55,15 @@
     public static final int ELEMENT_PHONE_SYMBOLS = 8;
     public static final int ELEMENT_NUMBER = 9;
 
+    public static final int FORM_FACTOR_PHONE = 0;
+    public static final int FORM_FACTOR_TABLET7 = 1;
+    public static final int FORM_FACTOR_TABLET10 = 2;
+
     private static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1;
 
     public final InputMethodSubtype mSubtype;
     public final Locale mLocale;
+    public final int mDeviceFormFactor;
     public final int mOrientation;
     public final int mWidth;
     public final int mMode;
@@ -72,11 +77,12 @@
 
     private final int mHashCode;
 
-    public KeyboardId(int elementId, InputMethodSubtype subtype, int orientation, int width,
-            int mode, EditorInfo editorInfo, boolean clobberSettingsKey, boolean shortcutKeyEnabled,
-            boolean hasShortcutKey, boolean languageSwitchKeyEnabled) {
+    public KeyboardId(int elementId, InputMethodSubtype subtype, int deviceFormFactor,
+            int orientation, int width, int mode, EditorInfo editorInfo, boolean clobberSettingsKey,
+            boolean shortcutKeyEnabled, boolean hasShortcutKey, boolean languageSwitchKeyEnabled) {
         mSubtype = subtype;
         mLocale = SubtypeLocale.getSubtypeLocale(subtype);
+        mDeviceFormFactor = deviceFormFactor;
         mOrientation = orientation;
         mWidth = width;
         mMode = mode;
@@ -94,6 +100,7 @@
 
     private static int computeHashCode(KeyboardId id) {
         return Arrays.hashCode(new Object[] {
+                id.mDeviceFormFactor,
                 id.mOrientation,
                 id.mElementId,
                 id.mMode,
@@ -115,7 +122,8 @@
     private boolean equals(KeyboardId other) {
         if (other == this)
             return true;
-        return other.mOrientation == mOrientation
+        return other.mDeviceFormFactor == mDeviceFormFactor
+                && other.mOrientation == mOrientation
                 && other.mElementId == mElementId
                 && other.mMode == mMode
                 && other.mWidth == mWidth
@@ -137,11 +145,13 @@
     }
 
     public boolean navigateNext() {
-        return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0;
+        return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0
+                || imeAction() == EditorInfo.IME_ACTION_NEXT;
     }
 
     public boolean navigatePrevious() {
-        return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0;
+        return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0
+                || imeAction() == EditorInfo.IME_ACTION_PREVIOUS;
     }
 
     public boolean passwordInput() {
@@ -182,11 +192,11 @@
 
     @Override
     public String toString() {
-        return String.format("[%s %s:%s %s%d %s %s %s%s%s%s%s%s%s%s]",
+        return String.format("[%s %s:%s %s-%s:%d %s %s %s%s%s%s%s%s%s%s]",
                 elementIdToName(mElementId),
                 mLocale,
                 mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
-                (mOrientation == 1 ? "port" : "land"), mWidth,
+                deviceFormFactor(mDeviceFormFactor), (mOrientation == 1 ? "port" : "land"), mWidth,
                 modeName(mMode),
                 imeAction(),
                 (navigateNext() ? "navigateNext" : ""),
@@ -224,6 +234,15 @@
         }
     }
 
+    public static String deviceFormFactor(int devoceFormFactor) {
+        switch (devoceFormFactor) {
+        case FORM_FACTOR_PHONE: return "phone";
+        case FORM_FACTOR_TABLET7: return "tablet7";
+        case FORM_FACTOR_TABLET10: return "tablet10";
+        default: return null;
+        }
+    }
+
     public static String modeName(int mode) {
         switch (mode) {
         case MODE_TEXT: return "text";
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index 8c72468..aaccf63 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -29,12 +29,16 @@
 import android.content.res.XmlResourceParser;
 import android.text.InputType;
 import android.util.Log;
+import android.util.SparseArray;
 import android.util.Xml;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.compat.EditorInfoCompatUtils;
-import com.android.inputmethod.keyboard.KeyboardLayoutSet.Params.ElementParams;
+import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
+import com.android.inputmethod.keyboard.internal.KeyboardParams;
+import com.android.inputmethod.keyboard.internal.KeysCache;
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.InputTypeUtils;
 import com.android.inputmethod.latin.LatinImeLogger;
@@ -70,41 +74,25 @@
     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 {
         public final KeyboardId mKeyboardId;
 
-        public KeyboardLayoutSetException(Throwable cause, KeyboardId keyboardId) {
+        public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
             super(cause);
             mKeyboardId = keyboardId;
         }
     }
 
-    public static class KeysCache {
-        private final HashMap<Key, Key> mMap;
-
-        public KeysCache() {
-            mMap = new HashMap<Key, Key>();
-        }
-
-        public void clear() {
-            mMap.clear();
-        }
-
-        public Key get(Key key) {
-            final Key existingKey = mMap.get(key);
-            if (existingKey != null) {
-                // Reuse the existing element that equals to "key" without adding "key" to the map.
-                return existingKey;
-            }
-            mMap.put(key, key);
-            return key;
-        }
+    private static class ElementParams {
+        int mKeyboardXmlId;
+        boolean mProximityCharsCorrectionEnabled;
+        public ElementParams() {}
     }
 
-    static class Params {
+    private static class Params {
         String mKeyboardLayoutSetName;
         int mMode;
         EditorInfo mEditorInfo;
@@ -114,16 +102,13 @@
         boolean mNoSettingsKey;
         boolean mLanguageSwitchKeyEnabled;
         InputMethodSubtype mSubtype;
+        int mDeviceFormFactor;
         int mOrientation;
         int mWidth;
-        // KeyboardLayoutSet element id to element's parameters map.
-        final HashMap<Integer, ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
-                new HashMap<Integer, ElementParams>();
-
-        static class ElementParams {
-            int mKeyboardXmlId;
-            boolean mProximityCharsCorrectionEnabled;
-        }
+        // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
+        final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
+                CollectionUtils.newSparseArray();
+        public Params() {}
     }
 
     public static void clearKeyboardCache() {
@@ -131,12 +116,12 @@
         sKeysCache.clear();
     }
 
-    private KeyboardLayoutSet(Context context, Params params) {
+    KeyboardLayoutSet(final Context context, final Params params) {
         mContext = context;
         mParams = params;
     }
 
-    public Keyboard getKeyboard(int baseKeyboardLayoutSetElementId) {
+    public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
         final int keyboardLayoutSetElementId;
         switch (mParams.mMode) {
         case KeyboardId.MODE_PHONE:
@@ -171,12 +156,12 @@
         }
     }
 
-    private Keyboard getKeyboard(ElementParams elementParams, final KeyboardId id) {
+    private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
         Keyboard keyboard = (ref == null) ? null : ref.get();
         if (keyboard == null) {
-            final Keyboard.Builder<Keyboard.Params> builder =
-                    new Keyboard.Builder<Keyboard.Params>(mContext, new Keyboard.Params());
+            final KeyboardBuilder<KeyboardParams> builder =
+                    new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams());
             if (id.isAlphabetKeyboard()) {
                 builder.setAutoGenerate(sKeysCache);
             }
@@ -203,16 +188,17 @@
     // KeyboardLayoutSet element id that is a key in keyboard_set.xml.  Also that file specifies
     // which XML layout should be used for each keyboard.  The KeyboardId is an internal key for
     // Keyboard object.
-    private KeyboardId getKeyboardId(int keyboardLayoutSetElementId) {
+    private KeyboardId getKeyboardId(final int keyboardLayoutSetElementId) {
         final Params params = mParams;
         final boolean isSymbols = (keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS
                 || keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED);
         final boolean noLanguage = SubtypeLocale.isNoLanguage(params.mSubtype);
         final boolean voiceKeyEnabled = params.mVoiceKeyEnabled && !noLanguage;
         final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != params.mVoiceKeyOnMain);
-        return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mOrientation,
-                params.mWidth, params.mMode, params.mEditorInfo, params.mNoSettingsKey,
-                voiceKeyEnabled, hasShortcutKey, params.mLanguageSwitchKeyEnabled);
+        return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mDeviceFormFactor,
+                params.mOrientation, params.mWidth, params.mMode, params.mEditorInfo,
+                params.mNoSettingsKey, voiceKeyEnabled, hasShortcutKey,
+                params.mLanguageSwitchKeyEnabled);
     }
 
     public static class Builder {
@@ -225,7 +211,7 @@
 
         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
 
-        public Builder(Context context, EditorInfo editorInfo) {
+        public Builder(final Context context, final EditorInfo editorInfo) {
             mContext = context;
             mPackageName = context.getPackageName();
             mResources = context.getResources();
@@ -238,13 +224,16 @@
                     mPackageName, NO_SETTINGS_KEY, mEditorInfo);
         }
 
-        public Builder setScreenGeometry(int orientation, int widthPixels) {
-            mParams.mOrientation = orientation;
-            mParams.mWidth = widthPixels;
+        public Builder setScreenGeometry(final int deviceFormFactor, final int orientation,
+                final int widthPixels) {
+            final Params params = mParams;
+            params.mDeviceFormFactor = deviceFormFactor;
+            params.mOrientation = orientation;
+            params.mWidth = widthPixels;
             return this;
         }
 
-        public Builder setSubtype(InputMethodSubtype subtype) {
+        public Builder setSubtype(final InputMethodSubtype subtype) {
             final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
             @SuppressWarnings("deprecation")
             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
@@ -261,8 +250,8 @@
             return this;
         }
 
-        public Builder setOptions(boolean voiceKeyEnabled, boolean voiceKeyOnMain,
-                boolean languageSwitchKeyEnabled) {
+        public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain,
+                final boolean languageSwitchKeyEnabled) {
             @SuppressWarnings("deprecation")
             final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
                     null, NO_MICROPHONE_COMPAT, mEditorInfo);
@@ -275,7 +264,7 @@
             return this;
         }
 
-        public void setTouchPositionCorrectionEnabled(boolean enabled) {
+        public void setTouchPositionCorrectionEnabled(final boolean enabled) {
             mParams.mTouchPositionCorrectionEnabled = enabled;
         }
 
@@ -296,7 +285,7 @@
             return new KeyboardLayoutSet(mContext, mParams);
         }
 
-        private void parseKeyboardLayoutSet(Resources res, int resId)
+        private void parseKeyboardLayoutSet(final Resources res, final int resId)
                 throws XmlPullParserException, IOException {
             final XmlResourceParser parser = res.getXml(resId);
             try {
@@ -316,7 +305,7 @@
             }
         }
 
-        private void parseKeyboardLayoutSetContent(XmlPullParser parser)
+        private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
                 throws XmlPullParserException, IOException {
             int event;
             while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
@@ -338,7 +327,7 @@
             }
         }
 
-        private void parseKeyboardLayoutSetElement(XmlPullParser parser)
+        private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
                 throws XmlPullParserException, IOException {
             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
                     R.styleable.KeyboardLayoutSet_Element);
@@ -365,7 +354,7 @@
             }
         }
 
-        private static int getKeyboardMode(EditorInfo editorInfo) {
+        private static int getKeyboardMode(final EditorInfo editorInfo) {
             if (editorInfo == null)
                 return KeyboardId.MODE_TEXT;
 
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 2e4ce19..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("Stne.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;
@@ -71,7 +70,7 @@
     private boolean mForceNonDistinctMultitouch;
 
     private InputView mCurrentInputView;
-    private LatinKeyboardView mKeyboardView;
+    private MainKeyboardView mKeyboardView;
     private LatinIME mLatinIME;
     private Resources mResources;
 
@@ -137,8 +136,9 @@
     public void loadKeyboard(EditorInfo editorInfo, SettingsValues settingsValues) {
         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
                 mThemeContext, editorInfo);
-        builder.setScreenGeometry(mThemeContext.getResources().getConfiguration().orientation,
-                mThemeContext.getResources().getDisplayMetrics().widthPixels);
+        final Resources res = mThemeContext.getResources();
+        builder.setScreenGeometry(res.getInteger(R.integer.config_device_form_factor),
+                res.getConfiguration().orientation, res.getDisplayMetrics().widthPixels);
         builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
         builder.setOptions(
                 settingsValues.isVoiceKeyEnabled(editorInfo),
@@ -169,19 +169,20 @@
     }
 
     private void setKeyboard(final Keyboard keyboard) {
-        final Keyboard oldKeyboard = mKeyboardView.getKeyboard();
-        mKeyboardView.setKeyboard(keyboard);
+        final MainKeyboardView keyboardView = mKeyboardView;
+        final Keyboard oldKeyboard = keyboardView.getKeyboard();
+        keyboardView.setKeyboard(keyboard);
         mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding);
-        mKeyboardView.setKeyPreviewPopupEnabled(
+        keyboardView.setKeyPreviewPopupEnabled(
                 SettingsValues.isKeyPreviewPopupEnabled(mPrefs, mResources),
                 SettingsValues.getKeyPreviewPopupDismissDelay(mPrefs, mResources));
-        mKeyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive);
-        mKeyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
+        keyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive);
+        keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
         final boolean subtypeChanged = (oldKeyboard == null)
                 || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
         final boolean needsToDisplayLanguage = mSubtypeSwitcher.needsToDisplayLanguage(
                 keyboard.mId.mLocale);
-        mKeyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage,
+        keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage,
                 ImfUtils.hasMultipleEnabledIMEsOrSubtypes(mLatinIME, true));
     }
 
@@ -265,7 +266,7 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void startDoubleTapTimer() {
-        final LatinKeyboardView keyboardView = getKeyboardView();
+        final MainKeyboardView keyboardView = getMainKeyboardView();
         if (keyboardView != null) {
             final TimerProxy timer = keyboardView.getTimerProxy();
             timer.startDoubleTapTimer();
@@ -275,7 +276,7 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void cancelDoubleTapTimer() {
-        final LatinKeyboardView keyboardView = getKeyboardView();
+        final MainKeyboardView keyboardView = getMainKeyboardView();
         if (keyboardView != null) {
             final TimerProxy timer = keyboardView.getTimerProxy();
             timer.cancelDoubleTapTimer();
@@ -285,7 +286,7 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public boolean isInDoubleTapTimeout() {
-        final LatinKeyboardView keyboardView = getKeyboardView();
+        final MainKeyboardView keyboardView = getMainKeyboardView();
         return (keyboardView != null)
                 ? keyboardView.getTimerProxy().isInDoubleTapTimeout() : false;
     }
@@ -293,7 +294,7 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void startLongPressTimer(int code) {
-        final LatinKeyboardView keyboardView = getKeyboardView();
+        final MainKeyboardView keyboardView = getMainKeyboardView();
         if (keyboardView != null) {
             final TimerProxy timer = keyboardView.getTimerProxy();
             timer.startLongPressTimer(code);
@@ -303,7 +304,7 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void cancelLongPressTimer() {
-        final LatinKeyboardView keyboardView = getKeyboardView();
+        final MainKeyboardView keyboardView = getMainKeyboardView();
         if (keyboardView != null) {
             final TimerProxy timer = keyboardView.getTimerProxy();
             timer.cancelLongPressTimer();
@@ -343,33 +344,24 @@
         mState.onCodeInput(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
     }
 
-    public LatinKeyboardView getKeyboardView() {
+    public MainKeyboardView getMainKeyboardView() {
         return mKeyboardView;
     }
 
-    public View onCreateInputView() {
+    public View onCreateInputView(boolean isHardwareAcceleratedDrawingEnabled) {
         if (mKeyboardView != null) {
             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 = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
+        mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
+        if (isHardwareAcceleratedDrawingEnabled) {
+            mKeyboardView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off?
+        }
         mKeyboardView.setKeyboardActionListener(mLatinIME);
         if (mForceNonDistinctMultitouch) {
             mKeyboardView.setDistinctMultitouch(false);
@@ -396,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 51a0f53..cf89567 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -25,62 +25,95 @@
 import android.graphics.Paint.Align;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
-import android.graphics.Region.Op;
+import android.graphics.Region;
 import android.graphics.Typeface;
 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;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import com.android.inputmethod.keyboard.internal.KeyDrawParams;
+import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
+import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
+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;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
 
-import java.util.HashMap;
 import java.util.HashSet;
 
 /**
  * A view that renders a virtual {@link Keyboard}.
  *
- * @attr ref R.styleable#KeyboardView_backgroundDimAlpha
  * @attr ref R.styleable#KeyboardView_keyBackground
- * @attr ref R.styleable#KeyboardView_keyLetterRatio
- * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio
- * @attr ref R.styleable#KeyboardView_keyLabelRatio
- * @attr ref R.styleable#KeyboardView_keyHintLetterRatio
- * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintRatio
- * @attr ref R.styleable#KeyboardView_keyHintLabelRatio
+ * @attr ref R.styleable#KeyboardView_moreKeysLayout
+ * @attr ref R.styleable#KeyboardView_keyPreviewLayout
+ * @attr ref R.styleable#KeyboardView_keyPreviewOffset
+ * @attr ref R.styleable#KeyboardView_keyPreviewHeight
+ * @attr ref R.styleable#KeyboardView_keyPreviewLingerTimeout
  * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding
  * @attr ref R.styleable#KeyboardView_keyHintLetterPadding
  * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding
  * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
- * @attr ref R.styleable#KeyboardView_keyTextStyle
- * @attr ref R.styleable#KeyboardView_keyPreviewLayout
- * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio
- * @attr ref R.styleable#KeyboardView_keyPreviewOffset
- * @attr ref R.styleable#KeyboardView_keyPreviewHeight
- * @attr ref R.styleable#KeyboardView_keyTextColor
- * @attr ref R.styleable#KeyboardView_keyTextColorDisabled
- * @attr ref R.styleable#KeyboardView_keyHintLetterColor
- * @attr ref R.styleable#KeyboardView_keyHintLabelColor
- * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintInactivatedColor
- * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintActivatedColor
- * @attr ref R.styleable#KeyboardView_shadowColor
- * @attr ref R.styleable#KeyboardView_shadowRadius
+ * @attr ref R.styleable#KeyboardView_keyTextShadowRadius
+ * @attr ref R.styleable#KeyboardView_backgroundDimAlpha
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextSize
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextColor
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextOffset
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewColor
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewHorizontalPadding
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewVerticalPadding
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewRoundRadius
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextLingerTimeout
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutStartDelay
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutDuration
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailUpdateInterval
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailColor
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailWidth
+ * @attr ref R.styleable#KeyboardView_verticalCorrection
+ * @attr ref R.styleable#Keyboard_Key_keyTypeface
+ * @attr ref R.styleable#Keyboard_Key_keyLetterSize
+ * @attr ref R.styleable#Keyboard_Key_keyLabelSize
+ * @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio
+ * @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio
+ * @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio
+ * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio
+ * @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio
+ * @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio
+ * @attr ref R.styleable#Keyboard_Key_keyTextColor
+ * @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled
+ * @attr ref R.styleable#Keyboard_Key_keyTextShadowColor
+ * @attr ref R.styleable#Keyboard_Key_keyHintLetterColor
+ * @attr ref R.styleable#Keyboard_Key_keyHintLabelColor
+ * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor
+ * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor
+ * @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor
  */
 public class KeyboardView extends View implements PointerTracker.DrawingProxy {
-    // Miscellaneous constants
-    private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
+    private static final String TAG = KeyboardView.class.getSimpleName();
 
     // XML attributes
+    private final KeyVisualAttributes mKeyVisualAttributes;
+    private final int mKeyLabelHorizontalPadding;
+    private final float mKeyHintLetterPadding;
+    private final float mKeyPopupHintLetterPadding;
+    private final float mKeyShiftedLetterHintPadding;
+    private final float mKeyTextShadowRadius;
     protected final float mVerticalCorrection;
     protected final int mMoreKeysLayout;
+    protected final Drawable mKeyBackground;
+    protected final Rect mKeyBackgroundPadding = new Rect();
     private final int mBackgroundDimAlpha;
 
     // HORIZONTAL ELLIPSIS "...", character for popup hint.
@@ -94,75 +127,104 @@
     // The maximum key label width in the proportion to the key width.
     private static final float MAX_LABEL_RATIO = 0.90f;
 
-    private final static int ALPHA_OPAQUE = 255;
-
     // Main keyboard
     private Keyboard mKeyboard;
-    protected final KeyDrawParams mKeyDrawParams;
+    protected final KeyDrawParams mKeyDrawParams = new KeyDrawParams();
+
+    // Preview placer view
+    private final PreviewPlacerView mPreviewPlacerView;
+    private final int[] mCoordinates = new int[2];
 
     // Key preview
+    private static final int PREVIEW_ALPHA = 240;
     private final int mKeyPreviewLayoutId;
-    protected final KeyPreviewDrawParams mKeyPreviewDrawParams;
+    private final int mPreviewOffset;
+    private final int mPreviewHeight;
+    private final int mPreviewLingerTimeout;
+    private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray();
+    protected final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams();
     private boolean mShowKeyPreviewPopup = true;
     private int mDelayAfterPreview;
-    private ViewGroup mPreviewPlacer;
+    // Background state set
+    private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = {
+        { // STATE_MIDDLE
+            EMPTY_STATE_SET,
+            { R.attr.state_has_morekeys }
+        },
+        { // STATE_LEFT
+            { R.attr.state_left_edge },
+            { R.attr.state_left_edge, R.attr.state_has_morekeys }
+        },
+        { // STATE_RIGHT
+            { R.attr.state_right_edge },
+            { R.attr.state_right_edge, R.attr.state_has_morekeys }
+        }
+    };
+    private static final int STATE_MIDDLE = 0;
+    private static final int STATE_LEFT = 1;
+    private static final int STATE_RIGHT = 2;
+    private static final int STATE_NORMAL = 0;
+    private static final int STATE_HAS_MOREKEYS = 1;
+    private static final int[] KEY_PREVIEW_BACKGROUND_DEFAULT_STATE =
+            KEY_PREVIEW_BACKGROUND_STATE_TABLE[STATE_MIDDLE][STATE_NORMAL];
 
     // 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>();
-    /** The region of invalidated keys */
-    private final Rect mInvalidatedKeysRect = new Rect();
+    private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet();
+    /** The working rectangle variable */
+    private final Rect mWorkingRect = new Rect();
     /** The keyboard bitmap buffer for faster updates */
-    private Bitmap mBuffer;
+    /** The clip region to draw keys */
+    private final Region mClipRegion = new Region();
+    private Bitmap mOffscreenBuffer;
     /** The canvas for the above mutable keyboard bitmap */
-    private Canvas mCanvas;
+    private final Canvas mOffscreenCanvas = new Canvas();
     private final Paint mPaint = new Paint();
     private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
-    // This map caches key label text height in pixel as value and key label text size as map key.
-    private static final HashMap<Integer, Float> sTextHeightCache =
-            new HashMap<Integer, Float>();
-    // This map caches key label text width in pixel as value and key label text size as map key.
-    private static final HashMap<Integer, Float> sTextWidthCache =
-            new HashMap<Integer, Float>();
+    // This sparse array caches key label text height in pixel indexed by key label text size.
+    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 = CollectionUtils.newSparseArray();
     private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
     private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
 
     private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
 
     public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> {
-        private static final int MSG_DISMISS_KEY_PREVIEW = 1;
+        private static final int MSG_DISMISS_KEY_PREVIEW = 0;
 
-        public DrawingHandler(KeyboardView outerInstance) {
+        public DrawingHandler(final KeyboardView outerInstance) {
             super(outerInstance);
         }
 
         @Override
-        public void handleMessage(Message msg) {
+        public void handleMessage(final Message msg) {
             final KeyboardView keyboardView = getOuterInstance();
             if (keyboardView == null) return;
             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;
             }
         }
 
-        public void dismissKeyPreview(long delay, PointerTracker tracker) {
+        public void dismissKeyPreview(final long delay, final PointerTracker tracker) {
             sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
         }
 
-        public void cancelDismissKeyPreview(PointerTracker tracker) {
+        public void cancelDismissKeyPreview(final PointerTracker tracker) {
             removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
         }
 
-        public void cancelAllDismissKeyPreviews() {
+        private void cancelAllDismissKeyPreviews() {
             removeMessages(MSG_DISMISS_KEY_PREVIEW);
         }
 
@@ -171,214 +233,60 @@
         }
     }
 
-    protected static class KeyDrawParams {
-        // XML attributes
-        public final int mKeyTextColor;
-        public final int mKeyTextInactivatedColor;
-        public final Typeface mKeyTextStyle;
-        public final float mKeyLabelHorizontalPadding;
-        public final float mKeyHintLetterPadding;
-        public final float mKeyPopupHintLetterPadding;
-        public final float mKeyShiftedLetterHintPadding;
-        public final int mShadowColor;
-        public final float mShadowRadius;
-        public final Drawable mKeyBackground;
-        public final int mKeyHintLetterColor;
-        public final int mKeyHintLabelColor;
-        public final int mKeyShiftedLetterHintInactivatedColor;
-        public final int mKeyShiftedLetterHintActivatedColor;
-
-        /* package */ final float mKeyLetterRatio;
-        private final float mKeyLargeLetterRatio;
-        private final float mKeyLabelRatio;
-        private final float mKeyLargeLabelRatio;
-        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;
-        public int mKeyLargeLetterSize;
-        public int mKeyLabelSize;
-        public int mKeyLargeLabelSize;
-        public int mKeyHintLetterSize;
-        public int mKeyShiftedLetterHintSize;
-        public int mKeyHintLabelSize;
-        public int mAnimAlpha;
-
-        public KeyDrawParams(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 (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);
-            }
-            mKeyLargeLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLabelRatio);
-            mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio);
-            mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
-            mKeyShiftedLetterHintRatio = getRatio(a,
-                    R.styleable.KeyboardView_keyShiftedLetterHintRatio);
-            mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio);
-            mKeyLabelHorizontalPadding = a.getDimension(
-                    R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
-            mKeyHintLetterPadding = a.getDimension(
-                    R.styleable.KeyboardView_keyHintLetterPadding, 0);
-            mKeyPopupHintLetterPadding = a.getDimension(
-                    R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
-            mKeyShiftedLetterHintPadding = a.getDimension(
-                    R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
-            mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
-            mKeyTextInactivatedColor = a.getColor(
-                    R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
-            mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
-            mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0);
-            mKeyShiftedLetterHintInactivatedColor = a.getColor(
-                    R.styleable.KeyboardView_keyShiftedLetterHintInactivatedColor, 0);
-            mKeyShiftedLetterHintActivatedColor = a.getColor(
-                    R.styleable.KeyboardView_keyShiftedLetterHintActivatedColor, 0);
-            mKeyTextStyle = Typeface.defaultFromStyle(
-                    a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
-            mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
-            mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
-
-            mKeyBackground.getPadding(mPadding);
-        }
-
-        public void updateKeyHeight(int keyHeight) {
-            if (mKeyLetterRatio >= 0.0f)
-                mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
-            if (mKeyLabelRatio >= 0.0f)
-                mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
-            mKeyLargeLabelSize = (int)(keyHeight * mKeyLargeLabelRatio);
-            mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio);
-            mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
-            mKeyShiftedLetterHintSize = (int)(keyHeight * mKeyShiftedLetterHintRatio);
-            mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio);
-        }
-
-        public void blendAlpha(Paint paint) {
-            final int color = paint.getColor();
-            paint.setARGB((paint.getAlpha() * mAnimAlpha) / ALPHA_OPAQUE,
-                    Color.red(color), Color.green(color), Color.blue(color));
-        }
-    }
-
-    /* package */ static class KeyPreviewDrawParams {
-        // XML attributes.
-        public final Drawable mPreviewBackground;
-        public final Drawable mPreviewLeftBackground;
-        public final Drawable mPreviewRightBackground;
-        public final int mPreviewTextColor;
-        public final int mPreviewOffset;
-        public final int mPreviewHeight;
-        public final Typeface mKeyTextStyle;
-        public final int mLingerTimeout;
-
-        private final float mPreviewTextRatio;
-        private final float mKeyLetterRatio;
-
-        // The graphical geometry of the key preview.
-        // <-width->
-        // +-------+   ^
-        // |       |   |
-        // |preview| height (visible)
-        // |       |   |
-        // +       + ^ v
-        //  \     /  |offset
-        // +-\   /-+ v
-        // |  +-+  |
-        // |parent |
-        // |    key|
-        // +-------+
-        // The background of a {@link TextView} being used for a key preview may have invisible
-        // paddings. To align the more keys keyboard panel's visible part with the visible part of
-        // the background, we need to record the width and height of key preview that don't include
-        // invisible paddings.
-        public int mPreviewVisibleWidth;
-        public int mPreviewVisibleHeight;
-        // The key preview may have an arbitrary offset and its background that may have a bottom
-        // padding. To align the more keys keyboard and the key preview we also need to record the
-        // offset between the top edge of parent key and the bottom of the visible part of key
-        // preview background.
-        public int mPreviewVisibleOffset;
-
-        public int mPreviewTextSize;
-        public int mKeyLetterSize;
-        public final int[] mCoordinates = new int[2];
-
-        private static final int PREVIEW_ALPHA = 240;
-
-        public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) {
-            mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground);
-            mPreviewLeftBackground = a.getDrawable(
-                    R.styleable.KeyboardView_keyPreviewLeftBackground);
-            mPreviewRightBackground = a.getDrawable(
-                    R.styleable.KeyboardView_keyPreviewRightBackground);
-            setAlpha(mPreviewBackground, PREVIEW_ALPHA);
-            setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA);
-            setAlpha(mPreviewRightBackground, PREVIEW_ALPHA);
-            mPreviewOffset = a.getDimensionPixelOffset(
-                    R.styleable.KeyboardView_keyPreviewOffset, 0);
-            mPreviewHeight = a.getDimensionPixelSize(
-                    R.styleable.KeyboardView_keyPreviewHeight, 80);
-            mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio);
-            mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
-            mLingerTimeout = a.getInt(R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
-
-            mKeyLetterRatio = keyDrawParams.mKeyLetterRatio;
-            mKeyTextStyle = keyDrawParams.mKeyTextStyle;
-        }
-
-        public void updateKeyHeight(int keyHeight) {
-            mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
-            mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
-        }
-
-        private static void setAlpha(Drawable drawable, int alpha) {
-            if (drawable == null)
-                return;
-            drawable.setAlpha(alpha);
-        }
-    }
-
-    public KeyboardView(Context context, AttributeSet attrs) {
+    public KeyboardView(final Context context, final AttributeSet attrs) {
         this(context, attrs, R.attr.keyboardViewStyle);
     }
 
-    public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
+    public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
         super(context, attrs, defStyle);
 
-        final TypedArray a = context.obtainStyledAttributes(
-                attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
-
-        mKeyDrawParams = new KeyDrawParams(a);
-        mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams);
-        mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
+        final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
+                R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
+        mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground);
+        mKeyBackground.getPadding(mKeyBackgroundPadding);
+        mPreviewOffset = keyboardViewAttr.getDimensionPixelOffset(
+                R.styleable.KeyboardView_keyPreviewOffset, 0);
+        mPreviewHeight = keyboardViewAttr.getDimensionPixelSize(
+                R.styleable.KeyboardView_keyPreviewHeight, 80);
+        mPreviewLingerTimeout = keyboardViewAttr.getInt(
+                R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
+        mDelayAfterPreview = mPreviewLingerTimeout;
+        mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset(
+                R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
+        mKeyHintLetterPadding = keyboardViewAttr.getDimension(
+                R.styleable.KeyboardView_keyHintLetterPadding, 0);
+        mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension(
+                R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
+        mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
+                R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
+        mKeyTextShadowRadius = keyboardViewAttr.getFloat(
+                R.styleable.KeyboardView_keyTextShadowRadius, 0.0f);
+        mKeyPreviewLayoutId = keyboardViewAttr.getResourceId(
+                R.styleable.KeyboardView_keyPreviewLayout, 0);
         if (mKeyPreviewLayoutId == 0) {
             mShowKeyPreviewPopup = false;
         }
-        mVerticalCorrection = a.getDimensionPixelOffset(
+        mVerticalCorrection = keyboardViewAttr.getDimension(
                 R.styleable.KeyboardView_verticalCorrection, 0);
-        mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0);
-        mBackgroundDimAlpha = a.getInt(R.styleable.KeyboardView_backgroundDimAlpha, 0);
-        a.recycle();
+        mMoreKeysLayout = keyboardViewAttr.getResourceId(
+                R.styleable.KeyboardView_moreKeysLayout, 0);
+        mBackgroundDimAlpha = keyboardViewAttr.getInt(
+                R.styleable.KeyboardView_backgroundDimAlpha, 0);
+        keyboardViewAttr.recycle();
 
-        mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout;
+        final TypedArray keyAttr = context.obtainStyledAttributes(attrs,
+                R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView);
+        mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
+        keyAttr.recycle();
 
+        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;
+    private static void blendAlpha(final Paint paint, final int alpha) {
+        final int color = paint.getColor();
+        paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE,
+                Color.red(color), Color.green(color), Color.blue(color));
     }
 
     /**
@@ -388,14 +296,14 @@
      * @see #getKeyboard()
      * @param keyboard the keyboard to display in this view
      */
-    public void setKeyboard(Keyboard keyboard) {
+    public void setKeyboard(final Keyboard keyboard) {
         mKeyboard = keyboard;
         LatinImeLogger.onSetKeyboard(keyboard);
         requestLayout();
         invalidateAllKeys();
         final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
-        mKeyDrawParams.updateKeyHeight(keyHeight);
-        mKeyPreviewDrawParams.updateKeyHeight(keyHeight);
+        mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
+        mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes);
     }
 
     /**
@@ -414,7 +322,7 @@
      * @param delay the delay after which the preview is dismissed
      * @see #isKeyPreviewPopupEnabled()
      */
-    public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
+    public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) {
         mShowKeyPreviewPopup = previewEnabled;
         mDelayAfterPreview = delay;
     }
@@ -428,8 +336,14 @@
         return mShowKeyPreviewPopup;
     }
 
+    public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+            final boolean drawsGestureFloatingPreviewText) {
+        mPreviewPlacerView.setGesturePreviewMode(
+                drawsGesturePreviewTrail, drawsGestureFloatingPreviewText);
+    }
+
     @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
         if (mKeyboard != null) {
             // The main keyboard expands to the display width.
             final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
@@ -440,73 +354,116 @@
     }
 
     @Override
-    public void onDraw(Canvas canvas) {
+    public void onDraw(final Canvas canvas) {
         super.onDraw(canvas);
-        if (mBufferNeedsUpdate || mBuffer == null) {
-            mBufferNeedsUpdate = false;
-            onBufferDraw();
+        if (canvas.isHardwareAccelerated()) {
+            onDrawKeyboard(canvas);
+            return;
         }
-        canvas.drawBitmap(mBuffer, 0, 0, null);
+
+        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
+                mOffscreenCanvas.setBitmap(mOffscreenBuffer);
+            }
+            onDrawKeyboard(mOffscreenCanvas);
+        }
+        canvas.drawBitmap(mOffscreenBuffer, 0, 0, null);
     }
 
-    private void onBufferDraw() {
+    private boolean maybeAllocateOffscreenBuffer() {
         final int width = getWidth();
         final int height = getHeight();
-        if (width == 0 || height == 0)
-            return;
-        if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
-            if (mBuffer != null)
-                mBuffer.recycle();
-            mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-            mInvalidateAllKeys = true;
-            if (mCanvas != null) {
-                mCanvas.setBitmap(mBuffer);
-            } else {
-                mCanvas = new Canvas(mBuffer);
+        if (width == 0 || height == 0) {
+            return false;
+        }
+        if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width
+                && mOffscreenBuffer.getHeight() == height) {
+            return false;
+        }
+        freeOffscreenBuffer();
+        mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        return true;
+    }
+
+    private void freeOffscreenBuffer() {
+        if (mOffscreenBuffer != null) {
+            mOffscreenBuffer.recycle();
+            mOffscreenBuffer = null;
+        }
+    }
+
+    private void onDrawKeyboard(final Canvas canvas) {
+        if (mKeyboard == null) return;
+
+        final int width = getWidth();
+        final int height = getHeight();
+        final Paint paint = mPaint;
+
+        // Calculate clip region and set.
+        final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty();
+        final boolean isHardwareAccelerated = canvas.isHardwareAccelerated();
+        // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
+        if (drawAllKeys || isHardwareAccelerated) {
+            mClipRegion.set(0, 0, width, height);
+        } else {
+            mClipRegion.setEmpty();
+            for (final Key key : mInvalidatedKeys) {
+                if (mKeyboard.hasKey(key)) {
+                    final int x = key.mX + getPaddingLeft();
+                    final int y = key.mY + getPaddingTop();
+                    mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight);
+                    mClipRegion.union(mWorkingRect);
+                }
+            }
+        }
+        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);
             }
         }
 
-        if (mKeyboard == null) return;
-
-        final Canvas canvas = mCanvas;
-        final Paint paint = mPaint;
-        final KeyDrawParams params = mKeyDrawParams;
-
-        if (mInvalidateAllKeys || mInvalidatedKeys.isEmpty()) {
-            mInvalidatedKeysRect.set(0, 0, width, height);
-            canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE);
-            canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
+        // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
+        if (drawAllKeys || isHardwareAccelerated) {
             // Draw all keys.
             for (final Key key : mKeyboard.mKeys) {
-                onDrawKey(key, canvas, paint, params);
-            }
-            if (mNeedsToDimEntireKeyboard) {
-                drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint);
+                onDrawKey(key, canvas, paint);
             }
         } else {
             // Draw invalidated keys.
             for (final Key key : mInvalidatedKeys) {
-                if (!mKeyboard.hasKey(key)) {
-                    continue;
-                }
-                final int x = key.mX + getPaddingLeft();
-                final int y = key.mY + getPaddingTop();
-                mInvalidatedKeysRect.set(x, y, x + key.mWidth, y + key.mHeight);
-                canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE);
-                canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
-                onDrawKey(key, canvas, paint, params);
-                if (mNeedsToDimEntireKeyboard) {
-                    drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint);
+                if (mKeyboard.hasKey(key)) {
+                    onDrawKey(key, canvas, paint);
                 }
             }
         }
 
+        // Overlay a dark rectangle to dim.
+        if (mNeedsToDimEntireKeyboard) {
+            paint.setColor(Color.BLACK);
+            paint.setAlpha(mBackgroundDimAlpha);
+            // Note: clipRegion() above is in effect if it was called.
+            canvas.drawRect(0, 0, width, height, paint);
+        }
+
+        // ResearchLogging indicator.
+        // TODO: Reimplement using a keyboard background image specific to the ResearchLogger,
+        // and remove this call.
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height);
+        }
+
         mInvalidatedKeys.clear();
-        mInvalidatedKeysRect.setEmpty();
         mInvalidateAllKeys = false;
     }
 
-    public void dimEntireKeyboard(boolean dimmed) {
+    public void dimEntireKeyboard(final boolean dimmed) {
         final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed;
         mNeedsToDimEntireKeyboard = dimmed;
         if (needsRedrawing) {
@@ -514,14 +471,18 @@
         }
     }
 
-    private void onDrawKey(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
-        final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft();
+    private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) {
+        final int keyDrawX = key.getDrawX() + getPaddingLeft();
         final int keyDrawY = key.mY + getPaddingTop();
         canvas.translate(keyDrawX, keyDrawY);
 
-        params.mAnimAlpha = ALPHA_OPAQUE;
+        final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap;
+        final KeyVisualAttributes attr = key.mKeyVisualAttributes;
+        final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr);
+        params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
+
         if (!key.isSpacer()) {
-            onDrawKeyBackground(key, canvas, params);
+            onDrawKeyBackground(key, canvas);
         }
         onDrawKeyTopVisuals(key, canvas, paint, params);
 
@@ -529,14 +490,14 @@
     }
 
     // Draw key background.
-    protected void onDrawKeyBackground(Key key, Canvas canvas, KeyDrawParams params) {
-        final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight
-                + params.mPadding.left + params.mPadding.right;
-        final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom;
-        final int bgX = -params.mPadding.left;
-        final int bgY = -params.mPadding.top;
+    protected void onDrawKeyBackground(final Key key, final Canvas canvas) {
+        final Rect padding = mKeyBackgroundPadding;
+        final int bgWidth = key.getDrawWidth() + padding.left + padding.right;
+        final int bgHeight = key.mHeight + padding.top + padding.bottom;
+        final int bgX = -padding.left;
+        final int bgY = -padding.top;
         final int[] drawableState = key.getCurrentDrawableState();
-        final Drawable background = params.mKeyBackground;
+        final Drawable background = mKeyBackground;
         background.setState(drawableState);
         final Rect bounds = background.getBounds();
         if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
@@ -551,8 +512,9 @@
     }
 
     // Draw key top visuals.
-    protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
-        final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+    protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
+            final KeyDrawParams params) {
+        final int keyWidth = key.getDrawWidth();
         final int keyHeight = key.mHeight;
         final float centerX = keyWidth * 0.5f;
         final float centerY = keyHeight * 0.5f;
@@ -566,12 +528,8 @@
         float positionX = centerX;
         if (key.mLabel != null) {
             final String label = key.mLabel;
-            // For characters, use large font. For labels like "Done", use smaller font.
-            paint.setTypeface(key.selectTypeface(params.mKeyTextStyle));
-            final int labelSize = key.selectTextSize(params.mKeyLetterSize,
-                    params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyLargeLabelSize,
-                    params.mKeyHintLabelSize);
-            paint.setTextSize(labelSize);
+            paint.setTypeface(key.selectTypeface(params));
+            paint.setTextSize(key.selectTextSize(params));
             final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint);
             final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint);
 
@@ -581,10 +539,10 @@
             // Horizontal label text alignment
             float labelWidth = 0;
             if (key.isAlignLeft()) {
-                positionX = (int)params.mKeyLabelHorizontalPadding;
+                positionX = mKeyLabelHorizontalPadding;
                 paint.setTextAlign(Align.LEFT);
             } else if (key.isAlignRight()) {
-                positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding;
+                positionX = keyWidth - mKeyLabelHorizontalPadding;
                 paint.setTextAlign(Align.RIGHT);
             } else if (key.isAlignLeftOfCenter()) {
                 // TODO: Parameterise this?
@@ -609,16 +567,15 @@
                         Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint)));
             }
 
-            paint.setColor(key.isShiftedLetterActivated()
-                    ? params.mKeyTextInactivatedColor : params.mKeyTextColor);
+            paint.setColor(key.selectTextColor(params));
             if (key.isEnabled()) {
                 // Set a drop shadow for the text
-                paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor);
+                paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor);
             } else {
                 // Make label invisible
                 paint.setColor(Color.TRANSPARENT);
             }
-            params.blendAlpha(paint);
+            blendAlpha(paint, params.mAnimAlpha);
             canvas.drawText(label, 0, label.length(), positionX, baseline, paint);
             // Turn off drop shadow and reset x-scale.
             paint.setShadowLayer(0, 0, 0, 0);
@@ -646,25 +603,10 @@
 
         // Draw hint label.
         if (key.mHintLabel != null) {
-            final String hint = key.mHintLabel;
-            final int hintColor;
-            final int hintSize;
-            if (key.hasHintLabel()) {
-                hintColor = params.mKeyHintLabelColor;
-                hintSize = params.mKeyHintLabelSize;
-                paint.setTypeface(Typeface.DEFAULT);
-            } else if (key.hasShiftedLetterHint()) {
-                hintColor = key.isShiftedLetterActivated()
-                        ? params.mKeyShiftedLetterHintActivatedColor
-                        : params.mKeyShiftedLetterHintInactivatedColor;
-                hintSize = params.mKeyShiftedLetterHintSize;
-            } else { // key.hasHintLetter()
-                hintColor = params.mKeyHintLetterColor;
-                hintSize = params.mKeyHintLetterSize;
-            }
-            paint.setColor(hintColor);
-            params.blendAlpha(paint);
-            paint.setTextSize(hintSize);
+            final String hintLabel = key.mHintLabel;
+            paint.setTextSize(key.selectHintTextSize(params));
+            paint.setColor(key.selectHintTextColor(params));
+            blendAlpha(paint, params.mAnimAlpha);
             final float hintX, hintY;
             if (key.hasHintLabel()) {
                 // The hint label is placed just right of the key label. Used mainly on
@@ -675,19 +617,19 @@
                 paint.setTextAlign(Align.LEFT);
             } else if (key.hasShiftedLetterHint()) {
                 // The hint label is placed at top-right corner of the key. Used mainly on tablet.
-                hintX = keyWidth - params.mKeyShiftedLetterHintPadding
+                hintX = keyWidth - mKeyShiftedLetterHintPadding
                         - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
                 paint.getFontMetrics(mFontMetrics);
                 hintY = -mFontMetrics.top;
                 paint.setTextAlign(Align.CENTER);
             } else { // key.hasHintLetter()
                 // The hint letter is placed at top-right corner of the key. Used mainly on phone.
-                hintX = keyWidth - params.mKeyHintLetterPadding
+                hintX = keyWidth - mKeyHintLetterPadding
                         - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2;
                 hintY = -paint.ascent();
                 paint.setTextAlign(Align.CENTER);
             }
-            canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint);
+            canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint);
 
             if (LatinImeLogger.sVISUALDEBUG) {
                 final Paint line = new Paint();
@@ -698,15 +640,15 @@
 
         // Draw key icon.
         if (key.mLabel == null && icon != null) {
-            final int iconWidth = icon.getIntrinsicWidth();
+            final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
             final int iconHeight = icon.getIntrinsicHeight();
             final int iconX, alignX;
             final int iconY = (keyHeight - iconHeight) / 2;
             if (key.isAlignLeft()) {
-                iconX = (int)params.mKeyLabelHorizontalPadding;
+                iconX = mKeyLabelHorizontalPadding;
                 alignX = iconX;
             } else if (key.isAlignRight()) {
-                iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth;
+                iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth;
                 alignX = iconX + iconWidth;
             } else { // Align center
                 iconX = (keyWidth - iconWidth) / 2;
@@ -727,17 +669,18 @@
     }
 
     // Draw popup hint "..." at the bottom right corner of the key.
-    protected void drawKeyPopupHint(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
-        final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+    protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint,
+            final KeyDrawParams params) {
+        final int keyWidth = key.getDrawWidth();
         final int keyHeight = key.mHeight;
 
-        paint.setTypeface(params.mKeyTextStyle);
-        paint.setTextSize(params.mKeyHintLetterSize);
-        paint.setColor(params.mKeyHintLabelColor);
+        paint.setTypeface(params.mTypeface);
+        paint.setTextSize(params.mHintLetterSize);
+        paint.setColor(params.mHintLabelColor);
         paint.setTextAlign(Align.CENTER);
-        final float hintX = keyWidth - params.mKeyHintLetterPadding
+        final float hintX = keyWidth - mKeyHintLetterPadding
                 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
-        final float hintY = keyHeight - params.mKeyPopupHintLetterPadding;
+        final float hintY = keyHeight - mKeyPopupHintLetterPadding;
         canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
 
         if (LatinImeLogger.sVISUALDEBUG) {
@@ -747,7 +690,7 @@
         }
     }
 
-    private static int getCharGeometryCacheKey(char referenceChar, Paint paint) {
+    private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) {
         final int labelSize = (int)paint.getTextSize();
         final Typeface face = paint.getTypeface();
         final int codePointOffset = referenceChar << 15;
@@ -765,8 +708,8 @@
     // Working variable for the following methods.
     private final Rect mTextBounds = new Rect();
 
-    private float getCharHeight(char[] referenceChar, Paint paint) {
-        final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
+    private float getCharHeight(final char[] referenceChar, final Paint paint) {
+        final int key = getCharGeometryCacheKey(referenceChar[0], paint);
         final Float cachedValue = sTextHeightCache.get(key);
         if (cachedValue != null)
             return cachedValue;
@@ -777,8 +720,8 @@
         return height;
     }
 
-    private float getCharWidth(char[] referenceChar, Paint paint) {
-        final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
+    private float getCharWidth(final char[] referenceChar, final Paint paint) {
+        final int key = getCharGeometryCacheKey(referenceChar[0], paint);
         final Float cachedValue = sTextWidthCache.get(key);
         if (cachedValue != null)
             return cachedValue;
@@ -789,36 +732,37 @@
         return width;
     }
 
-    public float getLabelWidth(String label, Paint paint) {
-        paint.getTextBounds(label.toString(), 0, label.length(), mTextBounds);
+    public float getLabelWidth(final String label, final Paint paint) {
+        paint.getTextBounds(label, 0, label.length(), mTextBounds);
         return mTextBounds.width();
     }
 
-    protected static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
-            int height) {
+    protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x,
+            final int y, final int width, final int height) {
         canvas.translate(x, y);
         icon.setBounds(0, 0, width, height);
         icon.draw(canvas);
         canvas.translate(-x, -y);
     }
 
-    private static void drawHorizontalLine(Canvas canvas, float y, float w, int color,
-            Paint paint) {
+    private static void drawHorizontalLine(final Canvas canvas, final float y, final float w,
+            final int color, final Paint paint) {
         paint.setStyle(Paint.Style.STROKE);
         paint.setStrokeWidth(1.0f);
         paint.setColor(color);
         canvas.drawLine(0, y, w, y, paint);
     }
 
-    private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) {
+    private static void drawVerticalLine(final Canvas canvas, final float x, final float h,
+            final int color, final Paint paint) {
         paint.setStyle(Paint.Style.STROKE);
         paint.setStrokeWidth(1.0f);
         paint.setColor(color);
         canvas.drawLine(x, 0, x, h, paint);
     }
 
-    private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color,
-            Paint paint) {
+    private static void drawRectangle(final Canvas canvas, final float x, final float y,
+            final float w, final float h, final int color, final Paint paint) {
         paint.setStyle(Paint.Style.STROKE);
         paint.setStrokeWidth(1.0f);
         paint.setColor(color);
@@ -827,57 +771,105 @@
         canvas.translate(-x, -y);
     }
 
-    // Overlay a dark rectangle to dim.
-    private static void drawDimRectangle(Canvas canvas, Rect rect, int alpha, Paint paint) {
-        paint.setColor(Color.BLACK);
-        paint.setAlpha(alpha);
-        canvas.drawRect(rect, paint);
-    }
-
     public Paint newDefaultLabelPaint() {
         final Paint paint = new Paint();
         paint.setAntiAlias(true);
-        paint.setTypeface(mKeyDrawParams.mKeyTextStyle);
-        paint.setTextSize(mKeyDrawParams.mKeyLabelSize);
+        paint.setTypeface(mKeyDrawParams.mTypeface);
+        paint.setTextSize(mKeyDrawParams.mLabelSize);
         return paint;
     }
 
     public void cancelAllMessages() {
         mDrawingHandler.cancelAllMessages();
-    }
-
-    // Called by {@link PointerTracker} constructor to create a TextView.
-    @Override
-    public TextView inflateKeyPreviewText() {
-        final Context context = getContext();
-        if (mKeyPreviewLayoutId != 0) {
-            return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
-        } else {
-            return new TextView(context);
+        if (mPreviewPlacerView != null) {
+            mPreviewPlacerView.cancelAllMessages();
         }
     }
 
+    private TextView getKeyPreviewText(final int pointerId) {
+        TextView previewText = mKeyPreviewTexts.get(pointerId);
+        if (previewText != null) {
+            return previewText;
+        }
+        final Context context = getContext();
+        if (mKeyPreviewLayoutId != 0) {
+            previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
+        } else {
+            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
-    public void dismissKeyPreview(PointerTracker tracker) {
+    public void dismissKeyPreview(final PointerTracker tracker) {
         mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
     }
 
-    private void addKeyPreview(TextView keyPreview) {
-        if (mPreviewPlacer == null) {
-            mPreviewPlacer = new RelativeLayout(getContext());
-            final ViewGroup windowContentView =
-                    (ViewGroup)getRootView().findViewById(android.R.id.content);
-            windowContentView.addView(mPreviewPlacer);
+    private void addKeyPreview(final TextView keyPreview) {
+        locatePreviewPlacerView();
+        mPreviewPlacerView.addView(
+                keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0));
+    }
+
+    private void locatePreviewPlacerView() {
+        if (mPreviewPlacerView.getParent() != null) {
+            return;
         }
-        mPreviewPlacer.addView(
-                keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacer, 0, 0));
+        final int[] viewOrigin = new int[2];
+        getLocationInWindow(viewOrigin);
+        mPreviewPlacerView.setOrigin(viewOrigin[0], viewOrigin[1]);
+        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(final String gestureFloatingPreviewText) {
+        locatePreviewPlacerView();
+        mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText);
+    }
+
+    public void dismissGestureFloatingPreviewText() {
+        locatePreviewPlacerView();
+        mPreviewPlacerView.dismissGestureFloatingPreviewText();
     }
 
     @Override
-    public void showKeyPreview(PointerTracker tracker) {
-        if (!mShowKeyPreviewPopup) return;
+    public void showGesturePreviewTrail(final PointerTracker tracker,
+            final boolean isOldestTracker) {
+        locatePreviewPlacerView();
+        mPreviewPlacerView.invalidatePointer(tracker, isOldestTracker);
+    }
 
-        final TextView previewText = tracker.getKeyPreviewText();
+    @Override
+    public void showKeyPreview(final PointerTracker tracker) {
+        final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
+        if (!mShowKeyPreviewPopup) {
+            previewParams.mPreviewVisibleOffset = -mKeyboard.mVerticalGap;
+            return;
+        }
+
+        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) {
@@ -889,21 +881,28 @@
         // If key is invalid or IME is already closed, we must not show key preview.
         // Trying to show key preview while root window is closed causes
         // WindowManager.BadTokenException.
-        if (key == null)
+        if (key == null) {
             return;
+        }
 
-        final KeyPreviewDrawParams params = mKeyPreviewDrawParams;
+        final KeyDrawParams drawParams = mKeyDrawParams;
+        previewText.setTextColor(drawParams.mPreviewTextColor);
+        final Drawable background = previewText.getBackground();
+        if (background != null) {
+            background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE);
+            background.setAlpha(PREVIEW_ALPHA);
+        }
         final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel;
-        // What we show as preview should match what we show on a key top in onBufferDraw().
+        // What we show as preview should match what we show on a key top in onDraw().
         if (label != null) {
             // TODO Should take care of temporaryShiftLabel here.
             previewText.setCompoundDrawables(null, null, null, null);
             if (StringUtils.codePointCount(label) > 1) {
-                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize);
+                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize);
                 previewText.setTypeface(Typeface.DEFAULT_BOLD);
             } else {
-                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize);
-                previewText.setTypeface(params.mKeyTextStyle);
+                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize);
+                previewText.setTypeface(key.selectTypeface(drawParams));
             }
             previewText.setText(label);
         } else {
@@ -911,48 +910,44 @@
                     key.getPreviewIcon(mKeyboard.mIconsSet));
             previewText.setText(null);
         }
-        previewText.setBackgroundDrawable(params.mPreviewBackground);
 
         previewText.measure(
                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+        final int keyDrawWidth = key.getDrawWidth();
         final int previewWidth = previewText.getMeasuredWidth();
-        final int previewHeight = params.mPreviewHeight;
+        final int previewHeight = mPreviewHeight;
         // The width and height of visible part of the key preview background. The content marker
         // of the background 9-patch have to cover the visible part of the background.
-        params.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
+        previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
                 - previewText.getPaddingRight();
-        params.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
+        previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
                 - previewText.getPaddingBottom();
         // The distance between the top edge of the parent key and the bottom of the visible part
         // of the key preview background.
-        params.mPreviewVisibleOffset = params.mPreviewOffset - previewText.getPaddingBottom();
-        getLocationInWindow(params.mCoordinates);
+        previewParams.mPreviewVisibleOffset = mPreviewOffset - previewText.getPaddingBottom();
+        getLocationInWindow(mCoordinates);
         // The key preview is horizontally aligned with the center of the visible part of the
         // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
         // the left/right background is used if such background is specified.
-        int previewX = key.mX + key.mVisualInsetsLeft - (previewWidth - keyDrawWidth) / 2
-                + params.mCoordinates[0];
+        final int statePosition;
+        int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0];
         if (previewX < 0) {
             previewX = 0;
-            if (params.mPreviewLeftBackground != null) {
-                previewText.setBackgroundDrawable(params.mPreviewLeftBackground);
-            }
+            statePosition = STATE_LEFT;
         } else if (previewX > getWidth() - previewWidth) {
             previewX = getWidth() - previewWidth;
-            if (params.mPreviewRightBackground != null) {
-                previewText.setBackgroundDrawable(params.mPreviewRightBackground);
-            }
+            statePosition = STATE_RIGHT;
+        } else {
+            statePosition = STATE_MIDDLE;
         }
         // The key preview is placed vertically above the top edge of the parent key with an
         // arbitrary offset.
-        final int previewY = key.mY - previewHeight + params.mPreviewOffset
-                + params.mCoordinates[1];
+        final int previewY = key.mY - previewHeight + mPreviewOffset + mCoordinates[1];
 
-        // Set the preview background state
-        previewText.getBackground().setState(
-                key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
-        previewText.setTextColor(params.mPreviewTextColor);
+        if (background != null) {
+            final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
+            background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
+        }
         ViewLayoutUtils.placeViewAt(
                 previewText, previewX, previewY, previewWidth, previewHeight);
         previewText.setVisibility(VISIBLE);
@@ -967,7 +962,6 @@
     public void invalidateAllKeys() {
         mInvalidatedKeys.clear();
         mInvalidateAllKeys = true;
-        mBufferNeedsUpdate = true;
         invalidate();
     }
 
@@ -979,19 +973,17 @@
      * @see #invalidateAllKeys
      */
     @Override
-    public void invalidateKey(Key key) {
+    public void invalidateKey(final Key key) {
         if (mInvalidateAllKeys) return;
         if (key == null) return;
         mInvalidatedKeys.add(key);
         final int x = key.mX + getPaddingLeft();
         final int y = key.mY + getPaddingTop();
-        mInvalidatedKeysRect.union(x, y, x + key.mWidth, y + key.mHeight);
-        mBufferNeedsUpdate = true;
-        invalidate(mInvalidatedKeysRect);
+        invalidate(x, y, x + key.mWidth, y + key.mHeight);
     }
 
     public void closing() {
-        PointerTracker.dismissAllKeyPreviews();
+        dismissAllKeyPreviews();
         cancelAllMessages();
 
         mInvalidateAllKeys = true;
@@ -1012,12 +1004,7 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         closing();
-        if (mPreviewPlacer != null) {
-            mPreviewPlacer.removeAllViews();
-        }
-        if (mBuffer != null) {
-            mBuffer.recycle();
-            mBuffer = null;
-        }
+        mPreviewPlacerView.removeAllViews();
+        freeOffscreenBuffer();
     }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
similarity index 73%
rename from java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
rename to java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 383298d..4ed0f58 100644
--- a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -43,16 +43,19 @@
 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
 import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
 import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.keyboard.internal.KeyDrawParams;
+import com.android.inputmethod.keyboard.internal.SuddenJumpingTouchEventHandler;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.ResourceUtils;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.StringUtils;
 import com.android.inputmethod.latin.SubtypeLocale;
-import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
 import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
 
 import java.util.Locale;
 import java.util.WeakHashMap;
@@ -60,13 +63,29 @@
 /**
  * A view that is responsible for detecting key presses and touch movements.
  *
- * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
- * @attr ref R.styleable#KeyboardView_verticalCorrection
- * @attr ref R.styleable#KeyboardView_popupLayout
+ * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled
+ * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon
+ * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio
+ * @attr ref R.styleable#MainKeyboardView_spacebarTextColor
+ * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor
+ * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha
+ * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator
+ * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator
+ * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator
+ * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance
+ * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime
+ * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance
+ * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable
+ * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout
+ * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval
+ * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout
+ * @attr ref R.styleable#MainKeyboardView_longPressShiftKeyTimeout
+ * @attr ref R.styleable#MainKeyboardView_ignoreAltCodeKeyTimeout
+ * @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint
  */
-public class LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
+public class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
         SuddenJumpingTouchEventHandler.ProcessMotionEvent {
-    private static final String TAG = LatinKeyboardView.class.getSimpleName();
+    private static final String TAG = MainKeyboardView.class.getSimpleName();
 
     // TODO: Kill process when the usability study mode was changed.
     private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
@@ -80,10 +99,9 @@
     // Stuff to draw language name on spacebar.
     private final int mLanguageOnSpacebarFinalAlpha;
     private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
-    private static final int ALPHA_OPAQUE = 255;
     private boolean mNeedsToDisplayLanguage;
     private boolean mHasMultipleEnabledIMEsOrSubtypes;
-    private int mLanguageOnSpacebarAnimAlpha = ALPHA_OPAQUE;
+    private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
     private final float mSpacebarTextRatio;
     private float mSpacebarTextSize;
     private final int mSpacebarTextColor;
@@ -99,7 +117,7 @@
     // Stuff to draw altCodeWhileTyping keys.
     private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
     private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
-    private int mAltCodeKeyWhileTypingAnimAlpha = ALPHA_OPAQUE;
+    private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
 
     // More keys keyboard
     private PopupWindow mMoreKeysWindow;
@@ -109,7 +127,6 @@
             new WeakHashMap<Key, MoreKeysPanel>();
     private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
 
-    private final PointerTrackerParams mPointerTrackerParams;
     private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
 
     protected KeyDetector mKeyDetector;
@@ -119,29 +136,49 @@
 
     private final KeyTimerHandler mKeyTimerHandler;
 
-    private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
+    private static class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView>
             implements TimerProxy {
+        private static final int MSG_TYPING_STATE_EXPIRED = 0;
         private static final int MSG_REPEAT_KEY = 1;
         private static final int MSG_LONGPRESS_KEY = 2;
         private static final int MSG_DOUBLE_TAP = 3;
-        private static final int MSG_TYPING_STATE_EXPIRED = 4;
 
-        private final KeyTimerParams mParams;
-        private boolean mInKeyRepeat;
+        private final int mKeyRepeatStartTimeout;
+        private final int mKeyRepeatInterval;
+        private final int mLongPressKeyTimeout;
+        private final int mLongPressShiftKeyTimeout;
+        private final int mIgnoreAltCodeKeyTimeout;
 
-        public KeyTimerHandler(LatinKeyboardView 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
-        public void handleMessage(Message msg) {
-            final LatinKeyboardView keyboardView = getOuterInstance();
+        public void handleMessage(final Message msg) {
+            final MainKeyboardView keyboardView = getOuterInstance();
             final PointerTracker tracker = (PointerTracker) msg.obj;
             switch (msg.what) {
+            case MSG_TYPING_STATE_EXPIRED:
+                startWhileTypingFadeinAnimation(keyboardView);
+                break;
             case MSG_REPEAT_KEY:
-                tracker.onRegisterKey(tracker.getKey());
-                startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval);
+                final Key currentKey = tracker.getKey();
+                if (currentKey != null && currentKey.mCode == msg.arg1) {
+                    tracker.onRegisterKey(currentKey);
+                    startKeyRepeatTimer(tracker, mKeyRepeatInterval);
+                }
                 break;
             case MSG_LONGPRESS_KEY:
                 if (tracker != null) {
@@ -150,39 +187,36 @@
                     KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
                 }
                 break;
-            case MSG_TYPING_STATE_EXPIRED:
-                cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
-                        keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
-                break;
             }
         }
 
-        private void startKeyRepeatTimer(PointerTracker tracker, long delay) {
-            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, tracker), delay);
+        private void startKeyRepeatTimer(final PointerTracker tracker, final long delay) {
+            final Key key = tracker.getKey();
+            if (key == null) return;
+            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
         }
 
         @Override
-        public void startKeyRepeatTimer(PointerTracker tracker) {
-            mInKeyRepeat = true;
-            startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout);
+        public void startKeyRepeatTimer(final PointerTracker tracker) {
+            startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout);
         }
 
         public void cancelKeyRepeatTimer() {
-            mInKeyRepeat = false;
             removeMessages(MSG_REPEAT_KEY);
         }
 
+        // TODO: Suppress layout changes in key repeat mode
         public boolean isInKeyRepeat() {
-            return mInKeyRepeat;
+            return hasMessages(MSG_REPEAT_KEY);
         }
 
         @Override
-        public void startLongPressTimer(int code) {
+        public void startLongPressTimer(final int code) {
             cancelLongPressTimer();
             final int delay;
             switch (code) {
             case Keyboard.CODE_SHIFT:
-                delay = mParams.mLongPressShiftKeyTimeout;
+                delay = mLongPressShiftKeyTimeout;
                 break;
             default:
                 delay = 0;
@@ -194,7 +228,7 @@
         }
 
         @Override
-        public void startLongPressTimer(PointerTracker tracker) {
+        public void startLongPressTimer(final PointerTracker tracker) {
             cancelLongPressTimer();
             if (tracker == null) {
                 return;
@@ -203,15 +237,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;
             }
@@ -225,7 +259,7 @@
             removeMessages(MSG_LONGPRESS_KEY);
         }
 
-        public static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
+        private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
                 final ObjectAnimator animatorToStart) {
             float startFraction = 0.0f;
             if (animatorToCancel.isStarted()) {
@@ -237,18 +271,39 @@
             animatorToStart.setCurrentPlayTime(startTime);
         }
 
+        private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) {
+            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
+                    keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
+        }
+
+        private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) {
+            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
+                    keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
+        }
+
         @Override
-        public void startTypingStateTimer() {
+        public void startTypingStateTimer(final Key typedKey) {
+            if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
+                return;
+            }
+
             final boolean isTyping = isTypingState();
             removeMessages(MSG_TYPING_STATE_EXPIRED);
+            final MainKeyboardView keyboardView = getOuterInstance();
+
+            // When user hits the space or the enter key, just cancel the while-typing timer.
+            final int typedCode = typedKey.mCode;
+            if (typedCode == Keyboard.CODE_SPACE || typedCode == Keyboard.CODE_ENTER) {
+                startWhileTypingFadeinAnimation(keyboardView);
+                return;
+            }
+
             sendMessageDelayed(
-                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout);
+                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
             if (isTyping) {
                 return;
             }
-            final LatinKeyboardView keyboardView = getOuterInstance();
-            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
-                    keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
+            startWhileTypingFadeoutAnimation(keyboardView);
         }
 
         @Override
@@ -283,99 +338,53 @@
         }
     }
 
-    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 latinKeyboardViewAttr) {
-            mSlidingKeyInputEnabled = latinKeyboardViewAttr.getBoolean(
-                    R.styleable.LatinKeyboardView_slidingKeyInputEnable, false);
-            mTouchNoiseThresholdTime = latinKeyboardViewAttr.getInt(
-                    R.styleable.LatinKeyboardView_touchNoiseThresholdTime, 0);
-            mTouchNoiseThresholdDistance = latinKeyboardViewAttr.getDimension(
-                    R.styleable.LatinKeyboardView_touchNoiseThresholdDistance, 0);
-        }
+    public MainKeyboardView(final Context context, final AttributeSet attrs) {
+        this(context, attrs, R.attr.mainKeyboardViewStyle);
     }
 
-    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 latinKeyboardViewAttr) {
-            mKeyRepeatStartTimeout = latinKeyboardViewAttr.getInt(
-                    R.styleable.LatinKeyboardView_keyRepeatStartTimeout, 0);
-            mKeyRepeatInterval = latinKeyboardViewAttr.getInt(
-                    R.styleable.LatinKeyboardView_keyRepeatInterval, 0);
-            mLongPressKeyTimeout = latinKeyboardViewAttr.getInt(
-                    R.styleable.LatinKeyboardView_longPressKeyTimeout, 0);
-            mLongPressShiftKeyTimeout = latinKeyboardViewAttr.getInt(
-                    R.styleable.LatinKeyboardView_longPressShiftKeyTimeout, 0);
-            mIgnoreAltCodeKeyTimeout = latinKeyboardViewAttr.getInt(
-                    R.styleable.LatinKeyboardView_ignoreAltCodeKeyTimeout, 0);
-        }
-    }
-
-    public LatinKeyboardView(Context context, AttributeSet attrs) {
-        this(context, attrs, R.attr.latinKeyboardViewStyle);
-    }
-
-    public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
+    public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
         super(context, attrs, defStyle);
 
         mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
 
         mHasDistinctMultitouch = context.getPackageManager()
                 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
+        final Resources res = getResources();
         final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
-                Utils.getDeviceOverrideValue(context.getResources(),
+                ResourceUtils.getDeviceOverrideValue(res,
                         R.array.phantom_sudden_move_event_device_list, "false"));
         PointerTracker.init(mHasDistinctMultitouch, needsPhantomSuddenMoveEventHack);
 
         final TypedArray a = context.obtainStyledAttributes(
-                attrs, R.styleable.LatinKeyboardView, defStyle, R.style.LatinKeyboardView);
+                attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView);
         mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
-                R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedEnabled, false);
+                R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false);
         mAutoCorrectionSpacebarLedIcon = a.getDrawable(
-                R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedIcon);
-        mSpacebarTextRatio = a.getFraction(R.styleable.LatinKeyboardView_spacebarTextRatio,
-                1000, 1000, 1) / 1000.0f;
-        mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboardView_spacebarTextColor, 0);
+                R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon);
+        mSpacebarTextRatio = a.getFraction(
+                R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f);
+        mSpacebarTextColor = a.getColor(R.styleable.MainKeyboardView_spacebarTextColor, 0);
         mSpacebarTextShadowColor = a.getColor(
-                R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
+                R.styleable.MainKeyboardView_spacebarTextShadowColor, 0);
         mLanguageOnSpacebarFinalAlpha = a.getInt(
-                R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha, ALPHA_OPAQUE);
+                R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha,
+                Constants.Color.ALPHA_OPAQUE);
         final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId(
-                R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
+                R.styleable.MainKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
         final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId(
-                R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
+                R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
         final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId(
-                R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
-
-        final KeyTimerParams keyTimerParams = new KeyTimerParams(a);
-        mPointerTrackerParams = new PointerTrackerParams(a);
+                R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
 
         final float keyHysteresisDistance = a.getDimension(
-                R.styleable.LatinKeyboardView_keyHysteresisDistance, 0);
+                R.styleable.MainKeyboardView_keyHysteresisDistance, 0);
         mKeyDetector = new KeyDetector(keyHysteresisDistance);
-        mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams);
+        mKeyTimerHandler = new KeyTimerHandler(this, a);
         mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean(
-                R.styleable.LatinKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
+                R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
+        PointerTracker.setParameters(a);
         a.recycle();
 
-        PointerTracker.setParameters(mPointerTrackerParams);
-
         mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
                 languageOnSpacebarFadeoutAnimatorResId, this);
         mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
@@ -384,7 +393,7 @@
                 altCodeKeyWhileTypingFadeinAnimatorResId, this);
     }
 
-    private ObjectAnimator loadObjectAnimator(int resId, Object target) {
+    private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
         if (resId == 0) return null;
         final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
                 getContext(), resId);
@@ -399,7 +408,7 @@
         return mLanguageOnSpacebarAnimAlpha;
     }
 
-    public void setLanguageOnSpacebarAnimAlpha(int alpha) {
+    public void setLanguageOnSpacebarAnimAlpha(final int alpha) {
         mLanguageOnSpacebarAnimAlpha = alpha;
         invalidateKey(mSpaceKey);
     }
@@ -408,12 +417,12 @@
         return mAltCodeKeyWhileTypingAnimAlpha;
     }
 
-    public void setAltCodeKeyWhileTypingAnimAlpha(int alpha) {
+    public void setAltCodeKeyWhileTypingAnimAlpha(final int alpha) {
         mAltCodeKeyWhileTypingAnimAlpha = alpha;
         updateAltCodeKeyWhileTyping();
     }
 
-    public void setKeyboardActionListener(KeyboardActionListener listener) {
+    public void setKeyboardActionListener(final KeyboardActionListener listener) {
         mKeyboardActionListener = listener;
         PointerTracker.setKeyboardActionListener(listener);
     }
@@ -450,9 +459,9 @@
      * @param keyboard the keyboard to display in this view
      */
     @Override
-    public void setKeyboard(Keyboard keyboard) {
-        // Remove any pending messages, except dismissing preview
-        mKeyTimerHandler.cancelKeyTimers();
+    public void setKeyboard(final Keyboard keyboard) {
+        // Remove any pending messages, except dismissing preview and key repeat.
+        mKeyTimerHandler.cancelLongPressTimer();
         super.setKeyboard(keyboard);
         mKeyDetector.setKeyboard(
                 keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
@@ -462,11 +471,11 @@
 
         mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
         mSpaceIcon = (mSpaceKey != null)
-                ? mSpaceKey.getIcon(keyboard.mIconsSet, ALPHA_OPAQUE) : null;
+                ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null;
         final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
         mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinKeyboardView_setKeyboard(keyboard);
+            ResearchLogger.mainKeyboardView_setKeyboard(keyboard);
         }
 
         // This always needs to be set since the accessibility state can
@@ -474,6 +483,15 @@
         AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard);
     }
 
+    // Note that this method is called from a non-UI thread.
+    public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
+        PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable);
+    }
+
+    public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
+        PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser);
+    }
+
     /**
      * Returns whether the device has distinct multi-touch panel.
      * @return true if the device has distinct multi-touch panel.
@@ -482,25 +500,29 @@
         return mHasDistinctMultitouch;
     }
 
-    public void setDistinctMultitouch(boolean hasDistinctMultitouch) {
+    public void setDistinctMultitouch(final boolean hasDistinctMultitouch) {
         mHasDistinctMultitouch = hasDistinctMultitouch;
     }
 
-    /**
-     * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
-     * codes for adjacent keys.  When disabled, only the primary key code will be
-     * reported.
-     * @param enabled whether or not the proximity correction is enabled
-     */
-    public void setProximityCorrectionEnabled(boolean enabled) {
-        mKeyDetector.setProximityCorrectionEnabled(enabled);
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        // Notify the research logger that the keyboard view has been attached.  This is needed
+        // 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(this);
+        }
     }
 
-    /**
-     * Returns true if proximity correction is enabled.
-     */
-    public boolean isProximityCorrectionEnabled() {
-        return mKeyDetector.isProximityCorrectionEnabled();
+    @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();
+        }
     }
 
     @Override
@@ -509,7 +531,8 @@
         super.cancelAllMessages();
     }
 
-    private boolean openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker) {
+    private boolean openMoreKeysKeyboardIfRequired(final Key parentKey,
+            final PointerTracker tracker) {
         // Check if we have a popup layout specified first.
         if (mMoreKeysLayout == 0) {
             return false;
@@ -524,7 +547,7 @@
     }
 
     // This default implementation returns a more keys panel.
-    protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
+    protected MoreKeysPanel onCreateMoreKeysPanel(final Key parentKey) {
         if (parentKey.mMoreKeys == null)
             return null;
 
@@ -550,9 +573,9 @@
      * @return true if the long press is handled, false otherwise. Subclasses should call the
      * method on the base class if the subclass doesn't wish to handle the call.
      */
-    protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
+    protected boolean onLongPress(final Key parentKey, final PointerTracker tracker) {
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinKeyboardView_onLongPress();
+            ResearchLogger.mainKeyboardView_onLongPress();
         }
         final int primaryCode = parentKey.mCode;
         if (parentKey.hasEmbeddedMoreKey()) {
@@ -574,21 +597,20 @@
         return openMoreKeysPanel(parentKey, tracker);
     }
 
-    private boolean invokeCustomRequest(int code) {
+    private boolean invokeCustomRequest(final int code) {
         return mKeyboardActionListener.onCustomRequest(code);
     }
 
-    private void invokeCodeInput(int primaryCode) {
-        mKeyboardActionListener.onCodeInput(primaryCode,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
-                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+    private void invokeCodeInput(final int primaryCode) {
+        mKeyboardActionListener.onCodeInput(
+                primaryCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
     }
 
-    private void invokeReleaseKey(int primaryCode) {
+    private void invokeReleaseKey(final int primaryCode) {
         mKeyboardActionListener.onReleaseKey(primaryCode, false);
     }
 
-    private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
+    private boolean openMoreKeysPanel(final Key parentKey, final PointerTracker tracker) {
         MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
         if (moreKeysPanel == null) {
             moreKeysPanel = onCreateMoreKeysPanel(parentKey);
@@ -614,9 +636,9 @@
         // The more keys keyboard is usually vertically aligned with the top edge of the parent key
         // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
         // aligned with the bottom edge of the visible part of the key preview.
-        final int pointY = parentKey.mY + (keyPreviewEnabled
-                ? mKeyPreviewDrawParams.mPreviewVisibleOffset
-                : -parentKey.mVerticalGap);
+        // {@code mPreviewVisibleOffset} has been set appropriately in
+        // {@link KeyboardView#showKeyPreview(PointerTracker)}.
+        final int pointY = parentKey.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset;
         moreKeysPanel.showMoreKeysPanel(
                 this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener);
         final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
@@ -639,7 +661,15 @@
     }
 
     @Override
-    public boolean onTouchEvent(MotionEvent me) {
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+            return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event);
+        }
+        return super.dispatchTouchEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(final MotionEvent me) {
         if (getKeyboard() == null) {
             return false;
         }
@@ -647,7 +677,7 @@
     }
 
     @Override
-    public boolean processMotionEvent(MotionEvent me) {
+    public boolean processMotionEvent(final MotionEvent me) {
         final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
         final int action = me.getActionMasked();
         final int pointerCount = me.getPointerCount();
@@ -703,7 +733,7 @@
             }
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime, index, id,
+            ResearchLogger.mainKeyboardView_processMotionEvent(me, action, eventTime, index, id,
                     x, y);
         }
 
@@ -755,15 +785,18 @@
                 final PointerTracker tracker = PointerTracker.getPointerTracker(
                         pointerId, this);
                 final int px, py;
+                final MotionEvent motionEvent;
                 if (mMoreKeysPanel != null
                         && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
                     px = mMoreKeysPanel.translateX((int)me.getX(i));
                     py = mMoreKeysPanel.translateY((int)me.getY(i));
+                    motionEvent = null;
                 } else {
                     px = (int)me.getX(i);
                     py = (int)me.getY(i);
+                    motionEvent = me;
                 }
-                tracker.onMoveEvent(px, py, eventTime);
+                tracker.onMoveEvent(px, py, eventTime, motionEvent);
                 if (ENABLE_USABILITY_STUDY_LOG) {
                     final float pointerSize = me.getSize(i);
                     final float pointerPressure = me.getPressure(i);
@@ -772,7 +805,7 @@
                             + pointerSize + "," + pointerPressure);
                 }
                 if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime,
+                    ResearchLogger.mainKeyboardView_processMotionEvent(me, action, eventTime,
                             i, pointerId, px, py);
                 }
             }
@@ -803,20 +836,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.
      *
@@ -825,7 +844,7 @@
      *         otherwise
      */
     @Override
-    public boolean dispatchHoverEvent(MotionEvent event) {
+    public boolean dispatchHoverEvent(final MotionEvent event) {
         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
             final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
             return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
@@ -835,7 +854,7 @@
         return false;
     }
 
-    public void updateShortcutKey(boolean available) {
+    public void updateShortcutKey(final boolean available) {
         final Keyboard keyboard = getKeyboard();
         if (keyboard == null) return;
         final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT);
@@ -852,8 +871,8 @@
         }
     }
 
-    public void startDisplayLanguageOnSpacebar(boolean subtypeChanged,
-            boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes) {
+    public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
+            final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) {
         mNeedsToDisplayLanguage = needsToDisplayLanguage;
         mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
         final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
@@ -861,7 +880,7 @@
             mNeedsToDisplayLanguage = false;
         } else {
             if (subtypeChanged && needsToDisplayLanguage) {
-                setLanguageOnSpacebarAnimAlpha(ALPHA_OPAQUE);
+                setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
                 if (animator.isStarted()) {
                     animator.cancel();
                 }
@@ -875,14 +894,15 @@
         invalidateKey(mSpaceKey);
     }
 
-    public void updateAutoCorrectionState(boolean isAutoCorrection) {
+    public void updateAutoCorrectionState(final boolean isAutoCorrection) {
         if (!mAutoCorrectionSpacebarLedEnabled) return;
         mAutoCorrectionSpacebarLedOn = isAutoCorrection;
         invalidateKey(mSpaceKey);
     }
 
     @Override
-    protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
+    protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
+            final KeyDrawParams params) {
         if (key.altCodeWhileTyping() && key.isEnabled()) {
             params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
         }
@@ -900,7 +920,7 @@
         }
     }
 
-    private boolean fitsTextIntoWidth(final int width, String text, Paint paint) {
+    private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) {
         paint.setTextScaleX(1.0f);
         final float textWidth = getLabelWidth(text, paint);
         if (textWidth < width) return true;
@@ -913,7 +933,7 @@
     }
 
     // Layout language name on spacebar.
-    private String layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype,
+    private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype,
             final int width) {
         // Choose appropriate language name to fit into the width.
         String text = getFullDisplayName(subtype, getResources());
@@ -934,7 +954,7 @@
         return "";
     }
 
-    private void drawSpacebar(Key key, Canvas canvas, Paint paint) {
+    private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) {
         final int width = key.mWidth;
         final int height = key.mHeight;
 
@@ -989,7 +1009,7 @@
     //  zz    azerty T      AZERTY    AZERTY
 
     // Get InputMethodSubtype's full display name in its locale.
-    static String getFullDisplayName(InputMethodSubtype subtype, Resources res) {
+    static String getFullDisplayName(final InputMethodSubtype subtype, final Resources res) {
         if (SubtypeLocale.isNoLanguage(subtype)) {
             return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
         }
@@ -998,7 +1018,7 @@
     }
 
     // Get InputMethodSubtype's short display name in its locale.
-    static String getShortDisplayName(InputMethodSubtype subtype) {
+    static String getShortDisplayName(final InputMethodSubtype subtype) {
         if (SubtypeLocale.isNoLanguage(subtype)) {
             return "";
         }
@@ -1007,7 +1027,7 @@
     }
 
     // Get InputMethodSubtype's middle display name in its locale.
-    static String getMiddleDisplayName(InputMethodSubtype subtype) {
+    static String getMiddleDisplayName(final InputMethodSubtype subtype) {
         if (SubtypeLocale.isNoLanguage(subtype)) {
             return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
         }
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
index a3741a2..c9af888 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -20,15 +20,17 @@
 import android.graphics.drawable.Drawable;
 import android.view.View;
 
-import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec;
+import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.keyboard.internal.KeyboardParams;
+import com.android.inputmethod.keyboard.internal.MoreKeySpec;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StringUtils;
 
 public class MoreKeysKeyboard extends Keyboard {
     private final int mDefaultKeyCoordX;
 
-    MoreKeysKeyboard(Builder.MoreKeysKeyboardParams params) {
+    MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
         super(params);
         mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
     }
@@ -37,220 +39,222 @@
         return mDefaultKeyCoordX;
     }
 
-    public static class Builder extends Keyboard.Builder<Builder.MoreKeysKeyboardParams> {
+    /* package for test */
+    static class MoreKeysKeyboardParams extends KeyboardParams {
+        public boolean mIsFixedOrder;
+        /* package */int mTopRowAdjustment;
+        public int mNumRows;
+        public int mNumColumns;
+        public int mTopKeys;
+        public int mLeftKeys;
+        public int mRightKeys; // includes default key.
+        public int mDividerWidth;
+        public int mColumnWidth;
+
+        public MoreKeysKeyboardParams() {
+            super();
+        }
+
+        /**
+         * Set keyboard parameters of more keys keyboard.
+         *
+         * @param numKeys number of keys in this more keys keyboard.
+         * @param maxColumns number of maximum columns of this more keys keyboard.
+         * @param keyWidth more keys keyboard key width in pixel, including horizontal gap.
+         * @param rowHeight more keys keyboard row height in pixel, including vertical gap.
+         * @param coordXInParent coordinate x of the key preview in parent keyboard.
+         * @param parentKeyboardWidth parent keyboard width in pixel.
+         * @param isFixedColumnOrder if true, more keys should be laid out in fixed order.
+         * @param dividerWidth width of divider, zero for no dividers.
+         */
+        public void setParameters(final int numKeys, final int maxColumns, final int keyWidth,
+                final int rowHeight, final int coordXInParent, final int parentKeyboardWidth,
+                final boolean isFixedColumnOrder, final int dividerWidth) {
+            mIsFixedOrder = isFixedColumnOrder;
+            if (parentKeyboardWidth / keyWidth < maxColumns) {
+                throw new IllegalArgumentException(
+                        "Keyboard is too small to hold more keys keyboard: "
+                                + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
+            }
+            mDefaultKeyWidth = keyWidth;
+            mDefaultRowHeight = rowHeight;
+
+            final int numRows = (numKeys + maxColumns - 1) / maxColumns;
+            mNumRows = numRows;
+            final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns)
+                    : getOptimizedColumns(numKeys, maxColumns);
+            mNumColumns = numColumns;
+            final int topKeys = numKeys % numColumns;
+            mTopKeys = topKeys == 0 ? numColumns : topKeys;
+
+            final int numLeftKeys = (numColumns - 1) / 2;
+            final int numRightKeys = numColumns - numLeftKeys; // including default key.
+            // Maximum number of keys we can layout both side of the parent key
+            final int maxLeftKeys = coordXInParent / keyWidth;
+            final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth;
+            int leftKeys, rightKeys;
+            if (numLeftKeys > maxLeftKeys) {
+                leftKeys = maxLeftKeys;
+                rightKeys = numColumns - leftKeys;
+            } else if (numRightKeys > maxRightKeys + 1) {
+                rightKeys = maxRightKeys + 1; // include default key
+                leftKeys = numColumns - rightKeys;
+            } else {
+                leftKeys = numLeftKeys;
+                rightKeys = numRightKeys;
+            }
+            // If the left keys fill the left side of the parent key, entire more keys keyboard
+            // should be shifted to the right unless the parent key is on the left edge.
+            if (maxLeftKeys == leftKeys && leftKeys > 0) {
+                leftKeys--;
+                rightKeys++;
+            }
+            // If the right keys fill the right side of the parent key, entire more keys
+            // should be shifted to the left unless the parent key is on the right edge.
+            if (maxRightKeys == rightKeys - 1 && rightKeys > 1) {
+                leftKeys++;
+                rightKeys--;
+            }
+            mLeftKeys = leftKeys;
+            mRightKeys = rightKeys;
+
+            // Adjustment of the top row.
+            mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment()
+                    : getAutoOrderTopRowAdjustment();
+            mDividerWidth = dividerWidth;
+            mColumnWidth = mDefaultKeyWidth + mDividerWidth;
+            mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
+            // Need to subtract the bottom row's gutter only.
+            mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
+                    + mTopPadding + mBottomPadding;
+        }
+
+        private int getFixedOrderTopRowAdjustment() {
+            if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns
+                    || mLeftKeys == 0  || mRightKeys == 1) {
+                return 0;
+            }
+            return -1;
+        }
+
+        private int getAutoOrderTopRowAdjustment() {
+            if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2
+                    || mLeftKeys == 0 || mRightKeys == 1) {
+                return 0;
+            }
+            return -1;
+        }
+
+        // Return key position according to column count (0 is default).
+        /* package */int getColumnPos(final int n) {
+            return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
+        }
+
+        private int getFixedOrderColumnPos(final int n) {
+            final int col = n % mNumColumns;
+            final int row = n / mNumColumns;
+            if (!isTopRow(row)) {
+                return col - mLeftKeys;
+            }
+            final int rightSideKeys = mTopKeys / 2;
+            final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
+            final int pos = col - leftSideKeys;
+            final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
+            final int numRightKeys = mRightKeys - 1;
+            if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
+                return pos;
+            } else if (numRightKeys < rightSideKeys) {
+                return pos - (rightSideKeys - numRightKeys);
+            } else { // numLeftKeys < leftSideKeys
+                return pos + (leftSideKeys - numLeftKeys);
+            }
+        }
+
+        private int getAutomaticColumnPos(final int n) {
+            final int col = n % mNumColumns;
+            final int row = n / mNumColumns;
+            int leftKeys = mLeftKeys;
+            if (isTopRow(row)) {
+                leftKeys += mTopRowAdjustment;
+            }
+            if (col == 0) {
+                // default position.
+                return 0;
+            }
+
+            int pos = 0;
+            int right = 1; // include default position key.
+            int left = 0;
+            int i = 0;
+            while (true) {
+                // Assign right key if available.
+                if (right < mRightKeys) {
+                    pos = right;
+                    right++;
+                    i++;
+                }
+                if (i >= col)
+                    break;
+                // Assign left key if available.
+                if (left < leftKeys) {
+                    left++;
+                    pos = -left;
+                    i++;
+                }
+                if (i >= col)
+                    break;
+            }
+            return pos;
+        }
+
+        private static int getTopRowEmptySlots(final int numKeys, final int numColumns) {
+            final int remainings = numKeys % numColumns;
+            return remainings == 0 ? 0 : numColumns - remainings;
+        }
+
+        private int getOptimizedColumns(final int numKeys, final int maxColumns) {
+            int numColumns = Math.min(numKeys, maxColumns);
+            while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
+                numColumns--;
+            }
+            return numColumns;
+        }
+
+        public int getDefaultKeyCoordX() {
+            return mLeftKeys * mColumnWidth;
+        }
+
+        public int getX(final int n, final int row) {
+            final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
+            if (isTopRow(row)) {
+                return x + mTopRowAdjustment * (mColumnWidth / 2);
+            }
+            return x;
+        }
+
+        public int getY(final int row) {
+            return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
+        }
+
+        public void markAsEdgeKey(final Key key, final int row) {
+            if (row == 0)
+                key.markAsTopEdge(this);
+            if (isTopRow(row))
+                key.markAsBottomEdge(this);
+        }
+
+        private boolean isTopRow(final int rowCount) {
+            return mNumRows > 1 && rowCount == mNumRows - 1;
+        }
+    }
+
+    public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> {
         private final Key mParentKey;
         private final Drawable mDivider;
 
         private static final float LABEL_PADDING_RATIO = 0.2f;
         private static final float DIVIDER_RATIO = 0.2f;
 
-        public static class MoreKeysKeyboardParams extends Keyboard.Params {
-            public boolean mIsFixedOrder;
-            /* package */int mTopRowAdjustment;
-            public int mNumRows;
-            public int mNumColumns;
-            public int mTopKeys;
-            public int mLeftKeys;
-            public int mRightKeys; // includes default key.
-            public int mDividerWidth;
-            public int mColumnWidth;
-
-            public MoreKeysKeyboardParams() {
-                super();
-            }
-
-            /**
-             * Set keyboard parameters of more keys keyboard.
-             *
-             * @param numKeys number of keys in this more keys keyboard.
-             * @param maxColumns number of maximum columns of this more keys keyboard.
-             * @param keyWidth more keys keyboard key width in pixel, including horizontal gap.
-             * @param rowHeight more keys keyboard row height in pixel, including vertical gap.
-             * @param coordXInParent coordinate x of the key preview in parent keyboard.
-             * @param parentKeyboardWidth parent keyboard width in pixel.
-             * @param isFixedColumnOrder if true, more keys should be laid out in fixed order.
-             * @param dividerWidth width of divider, zero for no dividers.
-             */
-            public void setParameters(int numKeys, int maxColumns, int keyWidth, int rowHeight,
-                    int coordXInParent, int parentKeyboardWidth, boolean isFixedColumnOrder,
-                    int dividerWidth) {
-                mIsFixedOrder = isFixedColumnOrder;
-                if (parentKeyboardWidth / keyWidth < maxColumns) {
-                    throw new IllegalArgumentException(
-                            "Keyboard is too small to hold more keys keyboard: "
-                                    + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
-                }
-                mDefaultKeyWidth = keyWidth;
-                mDefaultRowHeight = rowHeight;
-
-                final int numRows = (numKeys + maxColumns - 1) / maxColumns;
-                mNumRows = numRows;
-                final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns)
-                        : getOptimizedColumns(numKeys, maxColumns);
-                mNumColumns = numColumns;
-                final int topKeys = numKeys % numColumns;
-                mTopKeys = topKeys == 0 ? numColumns : topKeys;
-
-                final int numLeftKeys = (numColumns - 1) / 2;
-                final int numRightKeys = numColumns - numLeftKeys; // including default key.
-                // Maximum number of keys we can layout both side of the parent key
-                final int maxLeftKeys = coordXInParent / keyWidth;
-                final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth;
-                int leftKeys, rightKeys;
-                if (numLeftKeys > maxLeftKeys) {
-                    leftKeys = maxLeftKeys;
-                    rightKeys = numColumns - leftKeys;
-                } else if (numRightKeys > maxRightKeys + 1) {
-                    rightKeys = maxRightKeys + 1; // include default key
-                    leftKeys = numColumns - rightKeys;
-                } else {
-                    leftKeys = numLeftKeys;
-                    rightKeys = numRightKeys;
-                }
-                // If the left keys fill the left side of the parent key, entire more keys keyboard
-                // should be shifted to the right unless the parent key is on the left edge.
-                if (maxLeftKeys == leftKeys && leftKeys > 0) {
-                    leftKeys--;
-                    rightKeys++;
-                }
-                // If the right keys fill the right side of the parent key, entire more keys
-                // should be shifted to the left unless the parent key is on the right edge.
-                if (maxRightKeys == rightKeys - 1 && rightKeys > 1) {
-                    leftKeys++;
-                    rightKeys--;
-                }
-                mLeftKeys = leftKeys;
-                mRightKeys = rightKeys;
-
-                // Adjustment of the top row.
-                mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment()
-                        : getAutoOrderTopRowAdjustment();
-                mDividerWidth = dividerWidth;
-                mColumnWidth = mDefaultKeyWidth + mDividerWidth;
-                mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
-                // Need to subtract the bottom row's gutter only.
-                mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
-                        + mTopPadding + mBottomPadding;
-            }
-
-            private int getFixedOrderTopRowAdjustment() {
-                if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns
-                        || mLeftKeys == 0  || mRightKeys == 1) {
-                    return 0;
-                }
-                return -1;
-            }
-
-            private int getAutoOrderTopRowAdjustment() {
-                if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2
-                        || mLeftKeys == 0 || mRightKeys == 1) {
-                    return 0;
-                }
-                return -1;
-            }
-
-            // Return key position according to column count (0 is default).
-            /* package */int getColumnPos(int n) {
-                return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
-            }
-
-            private int getFixedOrderColumnPos(int n) {
-                final int col = n % mNumColumns;
-                final int row = n / mNumColumns;
-                if (!isTopRow(row)) {
-                    return col - mLeftKeys;
-                }
-                final int rightSideKeys = mTopKeys / 2;
-                final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
-                final int pos = col - leftSideKeys;
-                final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
-                final int numRightKeys = mRightKeys - 1;
-                if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
-                    return pos;
-                } else if (numRightKeys < rightSideKeys) {
-                    return pos - (rightSideKeys - numRightKeys);
-                } else { // numLeftKeys < leftSideKeys
-                    return pos + (leftSideKeys - numLeftKeys);
-                }
-            }
-
-            private int getAutomaticColumnPos(int n) {
-                final int col = n % mNumColumns;
-                final int row = n / mNumColumns;
-                int leftKeys = mLeftKeys;
-                if (isTopRow(row)) {
-                    leftKeys += mTopRowAdjustment;
-                }
-                if (col == 0) {
-                    // default position.
-                    return 0;
-                }
-
-                int pos = 0;
-                int right = 1; // include default position key.
-                int left = 0;
-                int i = 0;
-                while (true) {
-                    // Assign right key if available.
-                    if (right < mRightKeys) {
-                        pos = right;
-                        right++;
-                        i++;
-                    }
-                    if (i >= col)
-                        break;
-                    // Assign left key if available.
-                    if (left < leftKeys) {
-                        left++;
-                        pos = -left;
-                        i++;
-                    }
-                    if (i >= col)
-                        break;
-                }
-                return pos;
-            }
-
-            private static int getTopRowEmptySlots(int numKeys, int numColumns) {
-                final int remainings = numKeys % numColumns;
-                return remainings == 0 ? 0 : numColumns - remainings;
-            }
-
-            private int getOptimizedColumns(int numKeys, int maxColumns) {
-                int numColumns = Math.min(numKeys, maxColumns);
-                while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
-                    numColumns--;
-                }
-                return numColumns;
-            }
-
-            public int getDefaultKeyCoordX() {
-                return mLeftKeys * mColumnWidth;
-            }
-
-            public int getX(int n, int row) {
-                final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
-                if (isTopRow(row)) {
-                    return x + mTopRowAdjustment * (mColumnWidth / 2);
-                }
-                return x;
-            }
-
-            public int getY(int row) {
-                return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
-            }
-
-            public void markAsEdgeKey(Key key, int row) {
-                if (row == 0)
-                    key.markAsTopEdge(this);
-                if (isTopRow(row))
-                    key.markAsBottomEdge(this);
-            }
-
-            private boolean isTopRow(int rowCount) {
-                return mNumRows > 1 && rowCount == mNumRows - 1;
-            }
-        }
 
         /**
          * The builder of MoreKeysKeyboard.
@@ -258,7 +262,8 @@
          * @param parentKey the {@link Key} that invokes more keys keyboard.
          * @param parentKeyboardView the {@link KeyboardView} that contains the parentKey.
          */
-        public Builder(View containerView, Key parentKey, KeyboardView parentKeyboardView) {
+        public Builder(final View containerView, final Key parentKey,
+                final KeyboardView parentKeyboardView) {
             super(containerView.getContext(), new MoreKeysKeyboardParams());
             final Keyboard parentKeyboard = parentKeyboardView.getKeyboard();
             load(parentKeyboard.mMoreKeysTemplate, parentKeyboard.mId);
@@ -300,14 +305,14 @@
                     dividerWidth);
         }
 
-        private static int getMaxKeyWidth(KeyboardView view, Key parentKey, int minKeyWidth) {
+        private static int getMaxKeyWidth(final KeyboardView view, final Key parentKey,
+                final int minKeyWidth) {
             final int padding = (int)(view.getResources()
                     .getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding)
                     + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0));
             final Paint paint = view.newDefaultLabelPaint();
-            paint.setTextSize(parentKey.hasLabelsInMoreKeys()
-                    ? view.mKeyDrawParams.mKeyLabelSize
-                    : view.mKeyDrawParams.mKeyLetterSize);
+            paint.setTypeface(parentKey.selectTypeface(view.mKeyDrawParams));
+            paint.setTextSize(parentKey.selectMoreKeyTextSize(view.mKeyDrawParams));
             int maxWidth = minKeyWidth;
             for (final MoreKeySpec spec : parentKey.mMoreKeys) {
                 final String label = spec.mLabel;
@@ -322,24 +327,6 @@
             return maxWidth;
         }
 
-        private static class MoreKeyDivider extends Key.Spacer {
-            private final Drawable mIcon;
-
-            public MoreKeyDivider(MoreKeysKeyboardParams params, Drawable icon, int x, int y) {
-                super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight);
-                mIcon = icon;
-            }
-
-            @Override
-            public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
-                // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
-                // constructor.
-                // TODO: Drawable itself should have an alpha value.
-                mIcon.setAlpha(128);
-                return mIcon;
-            }
-        }
-
         @Override
         public MoreKeysKeyboard build() {
             final MoreKeysKeyboardParams params = mParams;
@@ -368,4 +355,23 @@
             return new MoreKeysKeyboard(params);
         }
     }
+
+    private static class MoreKeyDivider extends Key.Spacer {
+        private final Drawable mIcon;
+
+        public MoreKeyDivider(final MoreKeysKeyboardParams params, final Drawable icon,
+                final int x, final int y) {
+            super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight);
+            mIcon = icon;
+        }
+
+        @Override
+        public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
+            // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
+            // constructor.
+            // TODO: Drawable itself should have an alpha value.
+            mIcon.setAlpha(128);
+            return mIcon;
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index be7644f..e513a14 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -25,6 +25,8 @@
 
 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;
 
 /**
@@ -49,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
@@ -58,6 +61,21 @@
         }
 
         @Override
+        public void onStartBatchInput() {
+            mListener.onStartBatchInput();
+        }
+
+        @Override
+        public void onUpdateBatchInput(InputPointers batchPointers) {
+            mListener.onUpdateBatchInput(batchPointers);
+        }
+
+        @Override
+        public void onEndBatchInput(InputPointers batchPointers) {
+            mListener.onEndBatchInput(batchPointers);
+        }
+
+        @Override
         public void onCancelInput() {
             mListener.onCancelInput();
         }
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index babf6ec..d4902ec 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -16,26 +16,37 @@
 
 package com.android.inputmethod.keyboard;
 
+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.GestureStrokeWithPreviewPoints;
 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.ResearchLogger;
+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 {
+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;
     private static final boolean DEBUG_LISTENER = false;
     private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
 
+    /** 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;
+
     public interface KeyEventHandler {
         /**
          * Get KeyDetector object that is used for this PointerTracker.
@@ -65,13 +76,13 @@
 
     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 showGesturePreviewTrail(PointerTracker tracker, boolean isOldestTracker);
     }
 
     public interface TimerProxy {
-        public void startTypingStateTimer();
+        public void startTypingStateTimer(Key typedKey);
         public boolean isTypingState();
         public void startKeyRepeatTimer(PointerTracker tracker);
         public void startLongPressTimer(PointerTracker tracker);
@@ -84,7 +95,7 @@
 
         public static class Adapter implements TimerProxy {
             @Override
-            public void startTypingStateTimer() {}
+            public void startTypingStateTimer(Key typedKey) {}
             @Override
             public boolean isTypingState() { return false; }
             @Override
@@ -106,12 +117,39 @@
         }
     }
 
+    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 LatinKeyboardView.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 ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList();
     private static PointerTrackerQueue sPointerTrackerQueue;
 
     public final int mPointerId;
@@ -123,7 +161,14 @@
 
     private Keyboard mKeyboard;
     private int mKeyQuarterWidthSquared;
-    private final TextView mKeyPreviewText;
+
+    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;
@@ -148,9 +193,6 @@
     // true if this pointer has been long-pressed and is showing a more keys panel.
     private boolean mIsShowingMoreKeysPanel;
 
-    // true if this pointer is repeatable key
-    private boolean mIsRepeatableKey;
-
     // true if this pointer is in sliding key input
     boolean mIsInSlidingKeyInput;
 
@@ -164,6 +206,8 @@
     private static final KeyboardActionListener EMPTY_LISTENER =
             new KeyboardActionListener.Adapter();
 
+    private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints;
+
     public static void init(boolean hasDistinctMultitouch,
             boolean needsPhantomSuddenMoveEventHack) {
         if (hasDistinctMultitouch) {
@@ -172,17 +216,32 @@
             sPointerTrackerQueue = null;
         }
         sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
-
-        setParameters(LatinKeyboardView.PointerTrackerParams.DEFAULT);
+        sParams = PointerTrackerParams.DEFAULT;
     }
 
-    public static void setParameters(LatinKeyboardView.PointerTrackerParams params) {
-        sParams = params;
-        sTouchNoiseThresholdDistanceSquared = (int)(
-                params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance);
+    public static void setParameters(final TypedArray mainKeyboardViewAttr) {
+        sParams = new PointerTrackerParams(mainKeyboardViewAttr);
     }
 
-    public static PointerTracker getPointerTracker(final int id, KeyEventHandler handler) {
+    private static void updateGestureHandlingMode() {
+        sShouldHandleGesture = sMainDictionaryAvailable
+                && sGestureHandlingEnabledByInputField
+                && sGestureHandlingEnabledByUser
+                && !AccessibilityUtils.getInstance().isTouchExplorationEnabled();
+    }
+
+    // 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.
@@ -198,54 +257,58 @@
         return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false;
     }
 
-    public static void setKeyboardActionListener(KeyboardActionListener listener) {
-        for (final PointerTracker tracker : sTrackers) {
+    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);
             tracker.mListener = listener;
         }
     }
 
-    public static void setKeyDetector(KeyDetector keyDetector) {
-        for (final PointerTracker tracker : sTrackers) {
+    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);
             tracker.setKeyDetectorInner(keyDetector);
             // Mark that keyboard layout has been changed.
             tracker.mKeyboardLayoutHasBeenChanged = true;
         }
+        final Keyboard keyboard = keyDetector.getKeyboard();
+        sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput();
+        updateGestureHandlingMode();
     }
 
-    public static void dismissAllKeyPreviews() {
-        for (final PointerTracker tracker : sTrackers) {
-            tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
+    public static void setReleasedKeyGraphicsToAllKeys() {
+        final int trackersSize = sTrackers.size();
+        for (int i = 0; i < trackersSize; ++i) {
+            final PointerTracker tracker = sTrackers.get(i);
             tracker.setReleasedKeyGraphics(tracker.mCurrentKey);
         }
     }
 
-    public PointerTracker(int id, KeyEventHandler handler) {
-        if (handler == null)
+    private PointerTracker(final int id, final KeyEventHandler handler) {
+        if (handler == null) {
             throw new NullPointerException();
+        }
         mPointerId = id;
+        mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(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) {
+    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
+        if (sInGesture) {
+            return false;
+        }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
         if (DEBUG_LISTENER) {
             Log.d(TAG, "onPress    : " + KeyDetector.printableCode(key)
                     + " ignoreModifier=" + ignoreModifierKey
                     + " enabled=" + key.isEnabled());
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(key,
-                    ignoreModifierKey);
-        }
         if (ignoreModifierKey) {
             return false;
         }
@@ -253,9 +316,7 @@
             mListener.onPressKey(key.mCode);
             final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
             mKeyboardLayoutHasBeenChanged = false;
-            if (!key.altCodeWhileTyping() && !key.isModifier()) {
-                mTimerProxy.startTypingStateTimer();
-            }
+            mTimerProxy.startTypingStateTimer(key);
             return keyboardLayoutHasBeenChanged;
         }
         return false;
@@ -263,13 +324,14 @@
 
     // 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;
+        final int code = altersCode ? key.getAltCode() : primaryCode;
         if (DEBUG_LISTENER) {
-            Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code) + " text=" + key.mOutputText
-                    + " x=" + x + " y=" + y
+            Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code)
+                    + " text=" + key.getOutputText() + " x=" + x + " y=" + y
                     + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
                     + " enabled=" + key.isEnabled());
         }
@@ -283,7 +345,7 @@
         // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
         if (key.isEnabled() || altersCode) {
             if (code == Keyboard.CODE_OUTPUT_TEXT) {
-                mListener.onTextInput(key.mOutputText);
+                mListener.onTextInput(key.getOutputText());
             } else if (code != Keyboard.CODE_UNSPECIFIED) {
                 mListener.onCodeInput(code, x, y);
             }
@@ -292,7 +354,11 @@
 
     // 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) {
+    private void callListenerOnRelease(final Key key, final int primaryCode,
+            final boolean withSliding) {
+        if (sInGesture) {
+            return;
+        }
         final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
         if (DEBUG_LISTENER) {
             Log.d(TAG, "onRelease  : " + Keyboard.printableCode(primaryCode)
@@ -312,21 +378,31 @@
     }
 
     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();
+        mGestureStrokeWithPreviewPoints.setKeyboardGeometry(mKeyboard.mMostCommonKeyWidth);
+        final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY);
+        if (newKey != mCurrentKey) {
+            if (mDrawingProxy != null) {
+                setReleasedKeyGraphics(mCurrentKey);
+            }
+            // Keep {@link #mCurrentKey} that comes from previous keyboard.
+        }
         final int keyQuarterWidth = mKeyboard.mMostCommonKeyWidth / 4;
         mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth;
     }
 
+    @Override
     public boolean isInSlidingKeyInput() {
         return mIsInSlidingKeyInput;
     }
@@ -335,15 +411,16 @@
         return mCurrentKey;
     }
 
+    @Override
     public boolean isModifier() {
         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;
@@ -361,20 +438,20 @@
         }
 
         if (key.altCodeWhileTyping()) {
-            final int altCode = key.mAltCode;
+            final int altCode = key.getAltCode();
             final Key altKey = mKeyboard.getKey(altCode);
             if (altKey != null) {
                 updateReleaseKeyGraphics(altKey);
             }
             for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
-                if (k != key && k.mAltCode == altCode) {
+                if (k != key && k.getAltCode() == altCode) {
                     updateReleaseKeyGraphics(k);
                 }
             }
         }
     }
 
-    private void setPressedKeyGraphics(Key key) {
+    private void setPressedKeyGraphics(final Key key) {
         if (key == null) {
             return;
         }
@@ -386,7 +463,7 @@
             return;
         }
 
-        if (!key.noKeyPreview()) {
+        if (!key.noKeyPreview() && !sInGesture) {
             mDrawingProxy.showKeyPreview(this);
         }
         updatePressKeyGraphics(key);
@@ -400,29 +477,33 @@
         }
 
         if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) {
-            final int altCode = key.mAltCode;
+            final int altCode = key.getAltCode();
             final Key altKey = mKeyboard.getKey(altCode);
             if (altKey != null) {
                 updatePressKeyGraphics(altKey);
             }
             for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
-                if (k != key && k.mAltCode == altCode) {
+                if (k != key && k.getAltCode() == altCode) {
                     updatePressKeyGraphics(k);
                 }
             }
         }
     }
 
-    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 GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() {
+        return mGestureStrokeWithPreviewPoints;
+    }
+
     public int getLastX() {
         return mLastX;
     }
@@ -435,30 +516,98 @@
         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;
     }
 
-    public void processMotionEvent(int action, int x, int y, long eventTime,
-            KeyEventHandler handler) {
+    private static int getActivePointerTrackerCount() {
+        return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size();
+    }
+
+    private void mayStartBatchInput() {
+        if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) {
+            return;
+        }
+        if (DEBUG_LISTENER) {
+            Log.d(TAG, "onStartBatchInput");
+        }
+        sInGesture = true;
+        mListener.onStartBatchInput();
+        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
+        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
+    }
+
+    private void updateBatchInput(final long eventTime) {
+        synchronized (sAggregratedPointers) {
+            mGestureStrokeWithPreviewPoints.appendIncrementalBatchPoints(sAggregratedPointers);
+            final int size = sAggregratedPointers.getPointerSize();
+            if (size > sLastRecognitionPointSize
+                    && GestureStroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
+                sLastRecognitionPointSize = size;
+                sLastRecognitionTime = eventTime;
+                if (DEBUG_LISTENER) {
+                    Log.d(TAG, "onUpdateBatchInput: batchPoints=" + size);
+                }
+                mListener.onUpdateBatchInput(sAggregratedPointers);
+            }
+        }
+        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
+        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
+    }
+
+    private void mayEndBatchInput() {
+        synchronized (sAggregratedPointers) {
+            mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers);
+            mGestureStrokeWithPreviewPoints.reset();
+            if (getActivePointerTrackerCount() == 1) {
+                if (DEBUG_LISTENER) {
+                    Log.d(TAG, "onEndBatchInput: batchPoints="
+                            + sAggregratedPointers.getPointerSize());
+                }
+                sInGesture = false;
+                mListener.onEndBatchInput(sAggregratedPointers);
+                clearBatchInputPointsOfAllPointerTrackers();
+            }
+        }
+        final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
+        mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
+    }
+
+    private static void abortBatchInput() {
+        clearBatchInputPointsOfAllPointerTrackers();
+    }
+
+    private static void clearBatchInputPointsOfAllPointerTrackers() {
+        final int trackersSize = sTrackers.size();
+        for (int i = 0; i < trackersSize; ++i) {
+            final PointerTracker tracker = sTrackers.get(i);
+            tracker.mGestureStrokeWithPreviewPoints.reset();
+        }
+        sAggregratedPointers.reset();
+        sLastRecognitionPointSize = 0;
+        sLastRecognitionTime = 0;
+    }
+
+    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:
@@ -469,7 +618,7 @@
             onUpEvent(x, y, eventTime);
             break;
         case MotionEvent.ACTION_MOVE:
-            onMoveEvent(x, y, eventTime);
+            onMoveEvent(x, y, eventTime, null);
             break;
         case MotionEvent.ACTION_CANCEL:
             onCancelEvent(x, y, eventTime);
@@ -477,9 +626,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();
@@ -491,7 +642,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);
@@ -503,9 +654,9 @@
             }
         }
 
+        final Key key = getKeyOn(x, y);
         final PointerTrackerQueue queue = sPointerTrackerQueue;
         if (queue != null) {
-            final Key key = getKeyOn(x, y);
             if (key != null && key.isModifier()) {
                 // Before processing a down event of modifier key, all pointers already being
                 // tracked should be released.
@@ -514,9 +665,33 @@
             queue.add(this);
         }
         onDownEventInternal(x, y, eventTime);
+        if (!sShouldHandleGesture) {
+            return;
+        }
+        final int activePointerTrackerCount = getActivePointerTrackerCount();
+        if (activePointerTrackerCount == 1) {
+            mIsDetectingGesture = false;
+            // A gesture should start only from the letter key.
+            final boolean isAlphabetKeyboard = (mKeyboard != null)
+                    && mKeyboard.mId.isAlphabetKeyboard();
+            if (isAlphabetKeyboard && !mIsShowingMoreKeysPanel && key != null
+                    && Keyboard.isLetterCode(key.mCode)) {
+                sGestureFirstDownTime = eventTime;
+                onGestureDownEvent(x, y, eventTime);
+            }
+        } else if (sInGesture && activePointerTrackerCount > 1) {
+            onGestureDownEvent(x, y, eventTime);
+        }
     }
 
-    private void onDownEventInternal(int x, int y, long eventTime) {
+    private void onGestureDownEvent(final int x, final int y, final long eventTime) {
+        mIsDetectingGesture = true;
+        final int elapsedTimeFromFirstDown = (int)(eventTime - sGestureFirstDownTime);
+        mGestureStrokeWithPreviewPoints.addPoint(x, y, elapsedTimeFromFirstDown,
+                true /* isMajorEvent */);
+    }
+
+    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.
@@ -525,7 +700,6 @@
                 || mKeyDetector.alwaysAllowsSlidingInput();
         mKeyboardLayoutHasBeenChanged = false;
         mKeyAlreadyProcessed = false;
-        mIsRepeatableKey = false;
         mIsInSlidingKeyInput = false;
         mIgnoreModifierKey = false;
         if (key != null) {
@@ -542,23 +716,68 @@
         }
     }
 
-    private void startSlidingKeyInput(Key key) {
+    private void startSlidingKeyInput(final Key key) {
         if (!mIsInSlidingKeyInput) {
             mIgnoreModifierKey = key.isModifier();
         }
         mIsInSlidingKeyInput = true;
     }
 
-    public void onMoveEvent(int x, int y, long eventTime) {
-        if (DEBUG_MOVE_EVENT)
-            printTouchEvent("onMoveEvent:", x, y, eventTime);
-        if (mKeyAlreadyProcessed)
-            return;
+    private void onGestureMoveEvent(final int x, final int y, final long eventTime,
+            final boolean isMajorEvent, final Key key) {
+        final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
+        if (mIsDetectingGesture) {
+            mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isMajorEvent);
+            mayStartBatchInput();
+            if (sInGesture && key != null) {
+                updateBatchInput(eventTime);
+            }
+        }
+    }
 
+    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) {
+            return;
+        }
+
+        if (sShouldHandleGesture && me != null) {
+            // Add historical points to gesture path.
+            final int pointerIndex = me.findPointerIndex(mPointerId);
+            final int historicalSize = me.getHistorySize();
+            for (int h = 0; h < historicalSize; h++) {
+                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
+                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
+                final long historicalTime = me.getHistoricalEventTime(h);
+                onGestureMoveEvent(historicalX, historicalY, historicalTime,
+                        false /* isMajorEvent */, 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);
+
+        if (sShouldHandleGesture) {
+            // Register move event on gesture tracker.
+            onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, key);
+            if (sInGesture) {
+                mIgnoreModifierKey = true;
+                mTimerProxy.cancelLongPressTimer();
+                mIsInSlidingKeyInput = true;
+                mCurrentKey = null;
+                setReleasedKeyGraphics(oldKey);
+                return;
+            }
+        }
+
         if (key != null) {
             if (oldKey == null) {
                 // The pointer has been slid in to the new key, but the finger was not on any keys.
@@ -598,20 +817,34 @@
                     final int dx = x - lastX;
                     final int dy = y - lastY;
                     final int lastMoveSquared = dx * dx + dy * dy;
+                    // TODO: Should find a way to balance gesture detection and this hack.
                     if (sNeedsPhantomSuddenMoveEventHack
-                            && lastMoveSquared >= mKeyQuarterWidthSquared) {
+                            && lastMoveSquared >= mKeyQuarterWidthSquared
+                            && !mIsDetectingGesture) {
                         if (DEBUG_MODE) {
                             Log.w(TAG, String.format("onMoveEvent:"
                                     + " phantom sudden move event is translated to "
                                     + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y));
                         }
+                        // TODO: This should be moved to outside of this nested if-clause?
                         if (ProductionFlag.IS_EXPERIMENTAL) {
                             ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
                         }
                         onUpEventInternal();
                         onDownEventInternal(x, y, eventTime);
                     } else {
-                        mKeyAlreadyProcessed = true;
+                        // HACK: If there are currently multiple touches, register the key even if
+                        // the finger slides off the key. This defends against noise from some
+                        // 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 (getActivePointerTrackerCount() > 1 && sPointerTrackerQueue != null
+                                && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
+                            onUpEventInternal();
+                        }
+                        if (!mIsDetectingGesture) {
+                            mKeyAlreadyProcessed = true;
+                        }
                         setReleasedKeyGraphics(oldKey);
                     }
                 }
@@ -627,36 +860,45 @@
                 if (mIsAllowedSlidingKeyInput) {
                     onMoveToNewKey(key, x, y);
                 } else {
-                    mKeyAlreadyProcessed = true;
+                    if (!mIsDetectingGesture) {
+                        mKeyAlreadyProcessed = true;
+                    }
                 }
             }
         }
     }
 
-    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 (mCurrentKey != null && mCurrentKey.isModifier()) {
-                // Before processing an up event of modifier key, all pointers already being
-                // tracked should be released.
-                queue.releaseAllPointersExcept(this, eventTime);
-            } else {
-                queue.releaseAllPointersOlderThan(this, eventTime);
+            if (!sInGesture) {
+                if (mCurrentKey != null && mCurrentKey.isModifier()) {
+                    // Before processing an up event of modifier key, all pointers already being
+                    // tracked should be released.
+                    queue.releaseAllPointersExcept(this, eventTime);
+                } else {
+                    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.
-    public void onPhantomUpEvent(int x, int y, long eventTime) {
-        if (DEBUG_EVENT)
-            printTouchEvent("onPhntEvent:", x, y, eventTime);
+    @Override
+    public void onPhantomUpEvent(final long eventTime) {
+        if (DEBUG_EVENT) {
+            printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime);
+        }
         onUpEventInternal();
         mKeyAlreadyProcessed = true;
     }
@@ -664,23 +906,39 @@
     private void onUpEventInternal() {
         mTimerProxy.cancelKeyTimers();
         mIsInSlidingKeyInput = 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 (mKeyAlreadyProcessed)
+
+        if (sInGesture) {
+            if (currentKey != null) {
+                callListenerOnRelease(currentKey, currentKey.mCode, true);
+            }
+            mayEndBatchInput();
             return;
-        if (!mIsRepeatableKey) {
-            detectAndSendKey(mCurrentKey, mKeyX, mKeyY);
+        }
+        // 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) {
+            return;
+        }
+        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();
-        onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
         mIsShowingMoreKeysPanel = true;
+        onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
     }
 
     public void onLongPressed() {
@@ -692,9 +950,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) {
@@ -714,29 +973,25 @@
         }
     }
 
-    private void startRepeatKey(Key key) {
-        if (key != null && key.isRepeatable()) {
+    private void startRepeatKey(final Key key) {
+        if (key != null && key.isRepeatable() && !sInGesture) {
             onRegisterKey(key);
             mTimerProxy.startKeyRepeatTimer(this);
-            mIsRepeatableKey = true;
-        } else {
-            mIsRepeatableKey = false;
         }
     }
 
-    public void onRegisterKey(Key key) {
+    public void onRegisterKey(final Key key) {
         if (key != null) {
             detectAndSendKey(key, key.mX, key.mY);
-            if (!key.altCodeWhileTyping() && !key.isModifier()) {
-                mTimerProxy.startTypingStateTimer();
-            }
+            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) {
@@ -747,31 +1002,28 @@
         }
     }
 
-    private void startLongPressTimer(Key key) {
-        if (key != null && key.isLongPressEnabled()) {
+    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);
     }
 
-    private long mPreviousEventTime;
-
-    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);
-        final long delta = eventTime - mPreviousEventTime;
         Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title,
-                (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, code));
-        mPreviousEventTime = eventTime;
+                (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, eventTime, code));
     }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 1207c3f..e1b082c 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -18,15 +18,16 @@
 
 import android.graphics.Rect;
 import android.text.TextUtils;
-import android.util.FloatMath;
 
-import com.android.inputmethod.keyboard.Keyboard.Params.TouchPositionCorrection;
+import com.android.inputmethod.keyboard.internal.TouchPositionCorrection;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.JniUtils;
 
 import java.util.Arrays;
-import java.util.HashMap;
 
 public class ProximityInfo {
+    /** MAX_PROXIMITY_CHARS_SIZE must be the same as MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+     * in defines.h */
     public static final int MAX_PROXIMITY_CHARS_SIZE = 16;
     /** Number of key widths from current touch point to search for nearest keys. */
     private static float SEARCH_DISTANCE = 1.2f;
@@ -47,9 +48,10 @@
     private final Key[][] mGridNeighbors;
     private final String mLocaleStr;
 
-    ProximityInfo(String localeStr, int gridWidth, int gridHeight, int minWidth, int height,
-            int mostCommonKeyWidth, int mostCommonKeyHeight, final Key[] keys,
-            TouchPositionCorrection touchPositionCorrection) {
+    ProximityInfo(final String localeStr, final int gridWidth, final int gridHeight,
+            final int minWidth, final int height, final int mostCommonKeyWidth,
+            final int mostCommonKeyHeight, final Key[] keys,
+            final TouchPositionCorrection touchPositionCorrection) {
         if (TextUtils.isEmpty(localeStr)) {
             mLocaleStr = "";
         } else {
@@ -75,33 +77,12 @@
         mNativeProximityInfo = createNativeProximityInfo();
     }
 
-    // TODO: Remove this public constructor when the native part of the ProximityInfo becomes
-    // immutable.
-    // This public constructor aims only for test purpose.
-    public ProximityInfo(ProximityInfo o) {
-        mLocaleStr = o.mLocaleStr;
-        mGridWidth = o.mGridWidth;
-        mGridHeight = o.mGridHeight;
-        mGridSize = o.mGridSize;
-        mCellWidth = o.mCellWidth;
-        mCellHeight = o.mCellHeight;
-        mKeyboardMinWidth = o.mKeyboardMinWidth;
-        mKeyboardHeight = o.mKeyboardHeight;
-        mKeyHeight = o.mKeyHeight;
-        mMostCommonKeyWidth = o.mMostCommonKeyWidth;
-        mKeys = o.mKeys;
-        mTouchPositionCorrection = o.mTouchPositionCorrection;
-        mGridNeighbors = new Key[mGridSize][];
-        computeNearestNeighbors();
-        mNativeProximityInfo = createNativeProximityInfo();
-    }
-
     public static ProximityInfo createDummyProximityInfo() {
         return new ProximityInfo("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
     }
 
     public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximity,
-            int rowSize, int gridWidth, int gridHeight) {
+            final int rowSize, final int gridWidth, final int gridHeight) {
         final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo();
         spellCheckerProximityInfo.mNativeProximityInfo =
                 spellCheckerProximityInfo.setProximityInfoNative("",
@@ -132,7 +113,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) {
@@ -175,7 +156,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);
                 }
             }
@@ -209,10 +192,6 @@
     private void computeNearestNeighbors() {
         final int defaultWidth = mMostCommonKeyWidth;
         final Key[] keys = mKeys;
-        final HashMap<Integer, Key> keyCodeMap = new HashMap<Integer, Key>();
-        for (final Key key : keys) {
-            keyCodeMap.put(key.mCode, key);
-        }
         final int thresholdBase = (int) (defaultWidth * SEARCH_DISTANCE);
         final int threshold = thresholdBase * thresholdBase;
         // Round-up so we don't have any pixels outside the grid
@@ -257,7 +236,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/ViewLayoutUtils.java b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
index ee50470..dc12fa4 100644
--- a/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
+++ b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
@@ -22,7 +22,7 @@
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
-public class ViewLayoutUtils {
+public final class ViewLayoutUtils {
     private ViewLayoutUtils() {
         // This utility class is not publicly instantiable.
     }
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..699aaea
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
@@ -0,0 +1,297 @@
+/*
+ * 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.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.SystemClock;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResizableIntArray;
+
+final class GesturePreviewTrail {
+    private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
+
+    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;
+    // The wall time of the zero value in {@link #mEventTimes}
+    private long mCurrentTimeBase;
+    private int mTrailStartIndex;
+
+    static final class Params {
+        public final int mTrailColor;
+        public final float mTrailStartWidth;
+        public final float mTrailEndWidth;
+        public final int mFadeoutStartDelay;
+        public final int mFadeoutDuration;
+        public final int mUpdateInterval;
+
+        public final int mTrailLingerDuration;
+
+        public Params(final TypedArray keyboardViewAttr) {
+            mTrailColor = keyboardViewAttr.getColor(
+                    R.styleable.KeyboardView_gesturePreviewTrailColor, 0);
+            mTrailStartWidth = keyboardViewAttr.getDimension(
+                    R.styleable.KeyboardView_gesturePreviewTrailStartWidth, 0.0f);
+            mTrailEndWidth = keyboardViewAttr.getDimension(
+                    R.styleable.KeyboardView_gesturePreviewTrailEndWidth, 0.0f);
+            mFadeoutStartDelay = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
+            mFadeoutDuration = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0);
+            mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration;
+            mUpdateInterval = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0);
+        }
+    }
+
+    // Use this value as imaginary zero because x-coordinates may be zero.
+    private static final int DOWN_EVENT_MARKER = -128;
+
+    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 GestureStrokeWithPreviewPoints stroke, final long downTime) {
+        final int trailSize = mEventTimes.getLength();
+        stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
+        if (mEventTimes.getLength() == trailSize) {
+            return;
+        }
+        final int[] eventTimes = mEventTimes.getPrimitiveArray();
+        final int strokeId = stroke.getGestureStrokeId();
+        if (strokeId != mCurrentStrokeId) {
+            final int elapsedTime = (int)(downTime - mCurrentTimeBase);
+            for (int i = mTrailStartIndex; i < trailSize; i++) {
+                // Decay the previous strokes' event times.
+                eventTimes[i] -= elapsedTime;
+            }
+            final int[] xCoords = mXCoordinates.getPrimitiveArray();
+            final int downIndex = trailSize;
+            xCoords[downIndex] = markAsDownEvent(xCoords[downIndex]);
+            mCurrentTimeBase = downTime - eventTimes[downIndex];
+            mCurrentStrokeId = strokeId;
+        }
+    }
+
+    private static int getAlpha(final int elapsedTime, final Params params) {
+        if (elapsedTime < params.mFadeoutStartDelay) {
+            return Constants.Color.ALPHA_OPAQUE;
+        }
+        final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE
+                * (elapsedTime - params.mFadeoutStartDelay)
+                / params.mFadeoutDuration;
+        return Constants.Color.ALPHA_OPAQUE - decreasingAlpha;
+    }
+
+    private static float getWidth(final int elapsedTime, final Params params) {
+        return Math.max((params.mTrailLingerDuration - elapsedTime)
+                * (params.mTrailStartWidth - params.mTrailEndWidth)
+                / params.mTrailLingerDuration, 0.0f);
+    }
+
+    static final class WorkingSet {
+        // Input
+        // Previous point (P1) coordinates and trail radius.
+        public float p1x, p1y;
+        public float r1;
+        // Current point (P2) coordinates and trail radius.
+        public float p2x, p2y;
+        public float r2;
+
+        // Output
+        // Closing point of arc at P1.
+        public float p1ax, p1ay;
+        // Opening point of arc at P1.
+        public float p1bx, p1by;
+        // Opening point of arc at P2.
+        public float p2ax, p2ay;
+        // Closing point of arc at P2.
+        public float p2bx, p2by;
+        // Start angle of the trail arcs.
+        public float aa;
+        // Sweep angle of the trail arc at P1.
+        public float a1;
+        public RectF arc1 = new RectF();
+        // Sweep angle of the trail arc at P2.
+        public float a2;
+        public RectF arc2 = new RectF();
+    }
+
+    private static final float RIGHT_ANGLE = (float)(Math.PI / 2.0d);
+    private static final float RADIAN_TO_DEGREE = (float)(180.0d / Math.PI);
+
+    private static boolean calculatePathPoints(final WorkingSet w) {
+        final float dx = w.p2x - w.p1x;
+        final float dy = w.p2y - w.p1y;
+        // Distance of the points.
+        final double l = Math.hypot(dx, dy);
+        if (Double.compare(0.0d, l) == 0) {
+            return false;
+        }
+        // Angle of the line p1-p2
+        final float a = (float)Math.atan2(dy, dx);
+        // Difference of trail cap radius.
+        final float dr = w.r2 - w.r1;
+        // Variation of angle at trail cap.
+        final float ar = (float)Math.asin(dr / l);
+        // The start angle of trail cap arc at P1.
+        final float aa = a - (RIGHT_ANGLE + ar);
+        // The end angle of trail cap arc at P2.
+        final float ab = a + (RIGHT_ANGLE + ar);
+        final float cosa = (float)Math.cos(aa);
+        final float sina = (float)Math.sin(aa);
+        final float cosb = (float)Math.cos(ab);
+        final float sinb = (float)Math.sin(ab);
+        w.p1ax = w.p1x + w.r1 * cosa;
+        w.p1ay = w.p1y + w.r1 * sina;
+        w.p1bx = w.p1x + w.r1 * cosb;
+        w.p1by = w.p1y + w.r1 * sinb;
+        w.p2ax = w.p2x + w.r2 * cosa;
+        w.p2ay = w.p2y + w.r2 * sina;
+        w.p2bx = w.p2x + w.r2 * cosb;
+        w.p2by = w.p2y + w.r2 * sinb;
+        w.aa = aa * RADIAN_TO_DEGREE;
+        final float ar2degree = ar * 2.0f * RADIAN_TO_DEGREE;
+        w.a1 = -180.0f + ar2degree;
+        w.a2 = 180.0f + ar2degree;
+        w.arc1.set(w.p1x, w.p1y, w.p1x, w.p1y);
+        w.arc1.inset(-w.r1, -w.r1);
+        w.arc2.set(w.p2x, w.p2y, w.p2x, w.p2y);
+        w.arc2.inset(-w.r2, -w.r2);
+        return true;
+    }
+
+    private static void createPath(final Path path, final WorkingSet w) {
+        path.rewind();
+        // Trail cap at P1.
+        path.moveTo(w.p1x, w.p1y);
+        path.arcTo(w.arc1, w.aa, w.a1);
+        // Trail cap at P2.
+        path.moveTo(w.p2x, w.p2y);
+        path.arcTo(w.arc2, w.aa, w.a2);
+        // Two trapezoids connecting P1 and P2.
+        path.moveTo(w.p1ax, w.p1ay);
+        path.lineTo(w.p1x, w.p1y);
+        path.lineTo(w.p1bx, w.p1by);
+        path.lineTo(w.p2bx, w.p2by);
+        path.lineTo(w.p2x, w.p2y);
+        path.lineTo(w.p2ax, w.p2ay);
+        path.close();
+    }
+
+    private final WorkingSet mWorkingSet = new WorkingSet();
+    private final Path mPath = new Path();
+
+    /**
+     * 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
+     * @param outBoundsRect the bounding box of this gesture trail drawing
+     * @param params The drawing parameters of gesture preview trail
+     * @return true if some gesture preview trails remain to be drawn
+     */
+    public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
+            final Rect outBoundsRect, final Params params) {
+        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() - mCurrentTimeBase);
+        int startIndex;
+        for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) {
+            final int elapsedTime = sinceDown - eventTimes[startIndex];
+            // Skip too old trail points.
+            if (elapsedTime < params.mTrailLingerDuration) {
+                break;
+            }
+        }
+        mTrailStartIndex = startIndex;
+
+        if (startIndex < trailSize) {
+            paint.setColor(params.mTrailColor);
+            paint.setStyle(Paint.Style.FILL);
+            final Path path = mPath;
+            final WorkingSet w = mWorkingSet;
+            w.p1x = getXCoordValue(xCoords[startIndex]);
+            w.p1y = yCoords[startIndex];
+            int lastTime = sinceDown - eventTimes[startIndex];
+            float maxWidth = getWidth(lastTime, params);
+            w.r1 = maxWidth / 2.0f;
+            // Initialize bounds rectangle.
+            outBoundsRect.set((int)w.p1x, (int)w.p1y, (int)w.p1x, (int)w.p1y);
+            for (int i = startIndex + 1; i < trailSize - 1; i++) {
+                final int elapsedTime = sinceDown - eventTimes[i];
+                w.p2x = getXCoordValue(xCoords[i]);
+                w.p2y = yCoords[i];
+                // Draw trail line only when the current point isn't a down point.
+                if (!isDownEventXCoord(xCoords[i])) {
+                    final int alpha = getAlpha(elapsedTime, params);
+                    paint.setAlpha(alpha);
+                    final float width = getWidth(elapsedTime, params);
+                    w.r2 = width / 2.0f;
+                    if (calculatePathPoints(w)) {
+                        createPath(path, w);
+                        canvas.drawPath(path, paint);
+                        outBoundsRect.union((int)w.p2x, (int)w.p2y);
+                    }
+                    // Take union for the bounds.
+                    maxWidth = Math.max(maxWidth, width);
+                }
+                w.p1x = w.p2x;
+                w.p1y = w.p2y;
+                w.r1 = w.r2;
+                lastTime = elapsedTime;
+            }
+            // Take care of trail line width.
+            final int inset = -((int)maxWidth + 1);
+            outBoundsRect.inset(inset, inset);
+        }
+
+        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
new file mode 100644
index 0000000..73413f6
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -0,0 +1,209 @@
+/*
+ * 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.util.Log;
+
+import com.android.inputmethod.latin.InputPointers;
+import com.android.inputmethod.latin.ResizableIntArray;
+
+public class GestureStroke {
+    private static final String TAG = GestureStroke.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    public static final int DEFAULT_CAPACITY = 128;
+
+    private final int mPointerId;
+    private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
+    private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+    private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+    private int mIncrementalRecognitionSize;
+    private int mLastIncrementalBatchSize;
+    private long mLastMajorEventTime;
+    private int mLastMajorEventX;
+    private int mLastMajorEventY;
+
+    private int mKeyWidth;
+    private int mStartGestureLengthThreshold; // pixel
+    private int mMinGestureSamplingLength; // pixel
+    private int mGestureRecognitionSpeedThreshold; // pixel / sec
+    private int mDetectFastMoveSpeedThreshold; // pixel /sec
+    private int mDetectFastMoveTime;
+    private int mDetectFastMoveX;
+    private int mDetectFastMoveY;
+
+    // TODO: Move some of these to resource.
+    private static final float START_GESTURE_LENGTH_THRESHOLD_RATIO_TO_KEY_WIDTH = 0.75f;
+    private static final int START_GESTURE_DURATION_THRESHOLD = 70; // msec
+    private static final int MIN_GESTURE_RECOGNITION_TIME = 100; // msec
+    private static final float MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH = 1.0f / 6.0f;
+    private static final float GESTURE_RECOGNITION_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH =
+            5.5f; // keyWidth / sec
+    private static final float DETECT_FAST_MOVE_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH =
+            5.0f; // keyWidth / sec
+    private static final int MSEC_PER_SEC = 1000;
+
+    public static final boolean hasRecognitionTimePast(
+            final long currentTime, final long lastRecognitionTime) {
+        return currentTime > lastRecognitionTime + MIN_GESTURE_RECOGNITION_TIME;
+    }
+
+    public GestureStroke(final int pointerId) {
+        mPointerId = pointerId;
+    }
+
+    public void setKeyboardGeometry(final int keyWidth) {
+        mKeyWidth = keyWidth;
+        // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
+        mStartGestureLengthThreshold =
+                (int)(keyWidth * START_GESTURE_LENGTH_THRESHOLD_RATIO_TO_KEY_WIDTH);
+        mMinGestureSamplingLength = (int)(keyWidth * MIN_GESTURE_SAMPLING_RATIO_TO_KEY_WIDTH);
+        mGestureRecognitionSpeedThreshold =
+                (int)(keyWidth * GESTURE_RECOGNITION_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH);
+        mDetectFastMoveSpeedThreshold =
+                (int)(keyWidth * DETECT_FAST_MOVE_SPEED_THRESHOLD_RATIO_TO_KEY_WIDTH);
+        if (DEBUG) {
+            Log.d(TAG, "setKeyboardGeometry: keyWidth=" + keyWidth);
+        }
+    }
+
+    public boolean isStartOfAGesture() {
+        if (mDetectFastMoveTime == 0) {
+            return false;
+        }
+        final int size = mEventTimes.getLength();
+        if (size <= 0) {
+            return false;
+        }
+        final int lastIndex = size - 1;
+        final int deltaTime = mEventTimes.get(lastIndex) - mDetectFastMoveTime;
+        final int deltaLength = getDistance(
+                mXCoordinates.get(lastIndex), mYCoordinates.get(lastIndex),
+                mDetectFastMoveX, mDetectFastMoveY);
+        final boolean isStartOfAGesture = deltaTime > START_GESTURE_DURATION_THRESHOLD
+                && deltaLength > mStartGestureLengthThreshold;
+        if (DEBUG) {
+            Log.d(TAG, "isStartOfAGesture: dT=" + deltaTime + " dL=" + deltaLength
+                    + " points=" + size + (isStartOfAGesture ? " Detect start of a gesture" : ""));
+        }
+        return isStartOfAGesture;
+    }
+
+    public void reset() {
+        mIncrementalRecognitionSize = 0;
+        mLastIncrementalBatchSize = 0;
+        mEventTimes.setLength(0);
+        mXCoordinates.setLength(0);
+        mYCoordinates.setLength(0);
+        mLastMajorEventTime = 0;
+        mDetectFastMoveTime = 0;
+    }
+
+    private void appendPoint(final int x, final int y, final int time) {
+        mEventTimes.add(time);
+        mXCoordinates.add(x);
+        mYCoordinates.add(y);
+    }
+
+    private void updateMajorEvent(final int x, final int y, final int time) {
+        mLastMajorEventTime = time;
+        mLastMajorEventX = x;
+        mLastMajorEventY = y;
+    }
+
+    private int detectFastMove(final int x, final int y, final int time) {
+        final int size = mEventTimes.getLength();
+        final int lastIndex = size - 1;
+        final int lastX = mXCoordinates.get(lastIndex);
+        final int lastY = mYCoordinates.get(lastIndex);
+        final int dist = getDistance(lastX, lastY, x, y);
+        final int msecs = time - mEventTimes.get(lastIndex);
+        if (msecs > 0) {
+            final int pixels = getDistance(lastX, lastY, x, y);
+            final int pixelsPerSec = pixels * MSEC_PER_SEC;
+            if (DEBUG) {
+                final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
+                Log.d(TAG, String.format("Speed=%.3f keyWidth/sec", speed));
+            }
+            // Equivalent to (pixels / msecs < mStartSpeedThreshold / MSEC_PER_SEC)
+            if (mDetectFastMoveTime == 0 && pixelsPerSec > mDetectFastMoveSpeedThreshold * msecs) {
+                if (DEBUG) {
+                    Log.d(TAG, "Detect fast move: T=" + time + " points = " + size);
+                }
+                mDetectFastMoveTime = time;
+                mDetectFastMoveX = x;
+                mDetectFastMoveY = y;
+            }
+        }
+        return dist;
+    }
+
+    public void addPoint(final int x, final int y, final int time, final boolean isMajorEvent) {
+        final int size = mEventTimes.getLength();
+        if (size <= 0) {
+            // Down event
+            appendPoint(x, y, time);
+            updateMajorEvent(x, y, time);
+        } else {
+            final int dist = detectFastMove(x, y, time);
+            if (dist > mMinGestureSamplingLength) {
+                appendPoint(x, y, time);
+            }
+        }
+        if (isMajorEvent) {
+            updateIncrementalRecognitionSize(x, y, time);
+            updateMajorEvent(x, y, time);
+        }
+    }
+
+    private void updateIncrementalRecognitionSize(final int x, final int y, final int time) {
+        final int msecs = (int)(time - mLastMajorEventTime);
+        if (msecs <= 0) {
+            return;
+        }
+        final int pixels = getDistance(mLastMajorEventX, mLastMajorEventY, x, y);
+        final int pixelsPerSec = pixels * MSEC_PER_SEC;
+        // Equivalent to (pixels / msecs < mGestureRecognitionThreshold / MSEC_PER_SEC)
+        if (pixelsPerSec < mGestureRecognitionSpeedThreshold * msecs) {
+            mIncrementalRecognitionSize = mEventTimes.getLength();
+        }
+    }
+
+    public void appendAllBatchPoints(final InputPointers out) {
+        appendBatchPoints(out, mEventTimes.getLength());
+    }
+
+    public void appendIncrementalBatchPoints(final InputPointers out) {
+        appendBatchPoints(out, mIncrementalRecognitionSize);
+    }
+
+    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, length);
+        mLastIncrementalBatchSize = size;
+    }
+
+    private static int getDistance(final int x1, final int y1, final int x2, final int y2) {
+        final int dx = x1 - x2;
+        final int 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 (int)Math.sqrt(dx * dx + dy * dy);
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
new file mode 100644
index 0000000..3487b50
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import com.android.inputmethod.latin.ResizableIntArray;
+
+public class GestureStrokeWithPreviewPoints 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;
+
+    private int mMinPreviewSampleLengthSquare;
+    private int mLastX;
+    private int mLastY;
+
+    // TODO: Move this to resource.
+    private static final float MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH = 0.1f;
+
+    public GestureStrokeWithPreviewPoints(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 setKeyboardGeometry(final int keyWidth) {
+        super.setKeyboardGeometry(keyWidth);
+        final float sampleLength = keyWidth * MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH;
+        mMinPreviewSampleLengthSquare = (int)(sampleLength * sampleLength);
+    }
+
+    private boolean needsSampling(final int x, final int y) {
+        final int dx = x - mLastX;
+        final int dy = y - mLastY;
+        return dx * dx + dy * dy >= mMinPreviewSampleLengthSquare;
+    }
+
+    @Override
+    public void addPoint(final int x, final int y, final int time, final boolean isMajorEvent) {
+        super.addPoint(x, y, time, isMajorEvent);
+        if (isMajorEvent || needsSampling(x, y)) {
+            mPreviewEventTimes.add(time);
+            mPreviewXCoordinates.add(x);
+            mPreviewYCoordinates.add(y);
+            mLastX = x;
+            mLastY = 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/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
new file mode 100644
index 0000000..5dcd842
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
@@ -0,0 +1,140 @@
+/*
+ * 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.graphics.Typeface;
+
+import com.android.inputmethod.latin.ResourceUtils;
+
+public final class KeyDrawParams {
+    public Typeface mTypeface;
+
+    public int mLetterSize;
+    public int mLabelSize;
+    public int mLargeLetterSize;
+    public int mLargeLabelSize;
+    public int mHintLetterSize;
+    public int mShiftedLetterHintSize;
+    public int mHintLabelSize;
+    public int mPreviewTextSize;
+
+    public int mTextColor;
+    public int mTextInactivatedColor;
+    public int mTextShadowColor;
+    public int mHintLetterColor;
+    public int mHintLabelColor;
+    public int mShiftedLetterHintInactivatedColor;
+    public int mShiftedLetterHintActivatedColor;
+    public int mPreviewTextColor;
+
+    public int mAnimAlpha;
+
+    public KeyDrawParams() {}
+
+    private KeyDrawParams(final KeyDrawParams copyFrom) {
+        mTypeface = copyFrom.mTypeface;
+
+        mLetterSize = copyFrom.mLetterSize;
+        mLabelSize = copyFrom.mLabelSize;
+        mLargeLetterSize = copyFrom.mLargeLetterSize;
+        mLargeLabelSize = copyFrom.mLargeLabelSize;
+        mHintLetterSize = copyFrom.mHintLetterSize;
+        mShiftedLetterHintSize = copyFrom.mShiftedLetterHintSize;
+        mHintLabelSize = copyFrom.mHintLabelSize;
+        mPreviewTextSize = copyFrom.mPreviewTextSize;
+
+        mTextColor = copyFrom.mTextColor;
+        mTextInactivatedColor = copyFrom.mTextInactivatedColor;
+        mTextShadowColor = copyFrom.mTextShadowColor;
+        mHintLetterColor = copyFrom.mHintLetterColor;
+        mHintLabelColor = copyFrom.mHintLabelColor;
+        mShiftedLetterHintInactivatedColor = copyFrom.mShiftedLetterHintInactivatedColor;
+        mShiftedLetterHintActivatedColor = copyFrom.mShiftedLetterHintActivatedColor;
+        mPreviewTextColor = copyFrom.mPreviewTextColor;
+
+        mAnimAlpha = copyFrom.mAnimAlpha;
+    }
+
+    public void updateParams(final int keyHeight, final KeyVisualAttributes attr) {
+        if (attr == null) {
+            return;
+        }
+
+        if (attr.mTypeface != null) {
+            mTypeface = attr.mTypeface;
+        }
+
+        mLetterSize = selectTextSizeFromDimensionOrRatio(keyHeight,
+                attr.mLetterSize, attr.mLetterRatio, mLetterSize);
+        mLabelSize = selectTextSizeFromDimensionOrRatio(keyHeight,
+                attr.mLabelSize, attr.mLabelRatio, mLabelSize);
+        mLargeLabelSize = selectTextSize(keyHeight, attr.mLargeLabelRatio, mLargeLabelSize);
+        mLargeLetterSize = selectTextSize(keyHeight, attr.mLargeLetterRatio, mLargeLetterSize);
+        mHintLetterSize = selectTextSize(keyHeight, attr.mHintLetterRatio, mHintLetterSize);
+        mShiftedLetterHintSize = selectTextSize(keyHeight,
+                attr.mShiftedLetterHintRatio, mShiftedLetterHintSize);
+        mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize);
+        mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize);
+
+        mTextColor = selectColor(attr.mTextColor, mTextColor);
+        mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor);
+        mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor);
+        mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor);
+        mHintLabelColor = selectColor(attr.mHintLabelColor, mHintLabelColor);
+        mShiftedLetterHintInactivatedColor = selectColor(
+                attr.mShiftedLetterHintInactivatedColor, mShiftedLetterHintInactivatedColor);
+        mShiftedLetterHintActivatedColor = selectColor(
+                attr.mShiftedLetterHintActivatedColor, mShiftedLetterHintActivatedColor);
+        mPreviewTextColor = selectColor(attr.mPreviewTextColor, mPreviewTextColor);
+    }
+
+    public KeyDrawParams mayCloneAndUpdateParams(final int keyHeight,
+            final KeyVisualAttributes attr) {
+        if (attr == null) {
+            return this;
+        }
+        final KeyDrawParams newParams = new KeyDrawParams(this);
+        newParams.updateParams(keyHeight, attr);
+        return newParams;
+    }
+
+    private static final int selectTextSizeFromDimensionOrRatio(final int keyHeight,
+            final int dimens, final float ratio, final int defaultDimens) {
+        if (ResourceUtils.isValidDimensionPixelSize(dimens)) {
+            return dimens;
+        }
+        if (ResourceUtils.isValidFraction(ratio)) {
+            return (int)(keyHeight * ratio);
+        }
+        return defaultDimens;
+    }
+
+    private static final int selectTextSize(final int keyHeight, final float ratio,
+            final int defaultSize) {
+        if (ResourceUtils.isValidFraction(ratio)) {
+            return (int)(keyHeight * ratio);
+        }
+        return defaultSize;
+    }
+
+    private static final int selectColor(final int attrColor, final int defaultColor) {
+        if (attrColor != 0) {
+            return attrColor;
+        }
+        return defaultColor;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java
new file mode 100644
index 0000000..609d1a5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+public final class KeyPreviewDrawParams {
+    // The graphical geometry of the key preview.
+    // <-width->
+    // +-------+   ^
+    // |       |   |
+    // |preview| height (visible)
+    // |       |   |
+    // +       + ^ v
+    //  \     /  |offset
+    // +-\   /-+ v
+    // |  +-+  |
+    // |parent |
+    // |    key|
+    // +-------+
+    // The background of a {@link TextView} being used for a key preview may have invisible
+    // paddings. To align the more keys keyboard panel's visible part with the visible part of
+    // the background, we need to record the width and height of key preview that don't include
+    // invisible paddings.
+    public int mPreviewVisibleWidth;
+    public int mPreviewVisibleHeight;
+    // The key preview may have an arbitrary offset and its background that may have a bottom
+    // padding. To align the more keys keyboard and the key preview we also need to record the
+    // offset between the top edge of parent key and the bottom of the visible part of key
+    // preview background.
+    public int mPreviewVisibleOffset;
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index c4452a5..2a57caa 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;
 
@@ -55,38 +56,20 @@
     private static final char ESCAPE_CHAR = '\\';
     private static final char LABEL_END = '|';
     private static final String PREFIX_TEXT = "!text/";
-    private static final String PREFIX_ICON = "!icon/";
+    static final String PREFIX_ICON = "!icon/";
     private static final String PREFIX_CODE = "!code/";
     private static final String PREFIX_HEX = "0x";
     private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
 
-    public static class MoreKeySpec {
-        public final int mCode;
-        public final String mLabel;
-        public final String mOutputText;
-        public final int mIconId;
-
-        public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale,
-                final KeyboardCodesSet codesSet) {
-            mCode = toUpperCaseOfCodeForLocale(getCode(moreKeySpec, codesSet),
-                    needsToUpperCase, locale);
-            mLabel = toUpperCaseOfStringForLocale(getLabel(moreKeySpec),
-                    needsToUpperCase, locale);
-            mOutputText = toUpperCaseOfStringForLocale(getOutputText(moreKeySpec),
-                    needsToUpperCase, locale);
-            mIconId = getIconId(moreKeySpec);
-        }
-    }
-
     private KeySpecParser() {
         // Intentional empty constructor for utility class.
     }
 
-    private static boolean hasIcon(String moreKeySpec) {
+    private static boolean hasIcon(final String moreKeySpec) {
         return moreKeySpec.startsWith(PREFIX_ICON);
     }
 
-    private static boolean hasCode(String moreKeySpec) {
+    private static boolean hasCode(final String moreKeySpec) {
         final int end = indexOfLabelEnd(moreKeySpec, 0);
         if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
                 PREFIX_CODE, end + 1)) {
@@ -95,7 +78,7 @@
         return false;
     }
 
-    private static String parseEscape(String text) {
+    private static String parseEscape(final String text) {
         if (text.indexOf(ESCAPE_CHAR) < 0) {
             return text;
         }
@@ -114,7 +97,7 @@
         return sb.toString();
     }
 
-    private static int indexOfLabelEnd(String moreKeySpec, int start) {
+    private static int indexOfLabelEnd(final String moreKeySpec, final int start) {
         if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) {
             final int end = moreKeySpec.indexOf(LABEL_END, start);
             if (end == 0) {
@@ -135,7 +118,7 @@
         return -1;
     }
 
-    public static String getLabel(String moreKeySpec) {
+    public static String getLabel(final String moreKeySpec) {
         if (hasIcon(moreKeySpec)) {
             return null;
         }
@@ -148,7 +131,7 @@
         return label;
     }
 
-    private static String getOutputTextInternal(String moreKeySpec) {
+    private static String getOutputTextInternal(final String moreKeySpec) {
         final int end = indexOfLabelEnd(moreKeySpec, 0);
         if (end <= 0) {
             return null;
@@ -159,7 +142,7 @@
         return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1));
     }
 
-    static String getOutputText(String moreKeySpec) {
+    static String getOutputText(final String moreKeySpec) {
         if (hasCode(moreKeySpec)) {
             return null;
         }
@@ -183,7 +166,7 @@
         return (StringUtils.codePointCount(label) == 1) ? null : label;
     }
 
-    static int getCode(String moreKeySpec, KeyboardCodesSet codesSet) {
+    static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) {
         if (hasCode(moreKeySpec)) {
             final int end = indexOfLabelEnd(moreKeySpec, 0);
             if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
@@ -208,7 +191,8 @@
         return Keyboard.CODE_OUTPUT_TEXT;
     }
 
-    public static int parseCode(String text, KeyboardCodesSet codesSet, int defCode) {
+    public static int parseCode(final String text, final KeyboardCodesSet codesSet,
+            final int defCode) {
         if (text == null) return defCode;
         if (text.startsWith(PREFIX_CODE)) {
             return codesSet.getCode(text.substring(PREFIX_CODE.length()));
@@ -219,7 +203,7 @@
         }
     }
 
-    public static int getIconId(String moreKeySpec) {
+    public static int getIconId(final String moreKeySpec) {
         if (moreKeySpec != null && hasIcon(moreKeySpec)) {
             final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
             final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
@@ -229,7 +213,7 @@
         return KeyboardIconsSet.ICON_UNDEFINED;
     }
 
-    private static <T> ArrayList<T> arrayAsList(T[] array, int start, int end) {
+    private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) {
         if (array == null) {
             throw new NullPointerException();
         }
@@ -237,7 +221,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]);
         }
@@ -246,7 +230,7 @@
 
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
 
-    private static String[] filterOutEmptyString(String[] array) {
+    private static String[] filterOutEmptyString(final String[] array) {
         if (array == null) {
             return EMPTY_STRING_ARRAY;
         }
@@ -267,8 +251,8 @@
         return out.toArray(new String[out.size()]);
     }
 
-    public static String[] insertAdditionalMoreKeys(String[] moreKeySpecs,
-            String[] additionalMoreKeySpecs) {
+    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
+            final String[] additionalMoreKeySpecs) {
         final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
         final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
         final int moreKeysCount = moreKeys.length;
@@ -335,12 +319,13 @@
 
     @SuppressWarnings("serial")
     public static class KeySpecParserError extends RuntimeException {
-        public KeySpecParserError(String message) {
+        public KeySpecParserError(final String message) {
             super(message);
         }
     }
 
-    public static String resolveTextReference(String rawText, KeyboardTextsSet textsSet) {
+    public static String resolveTextReference(final String rawText,
+            final KeyboardTextsSet textsSet) {
         int level = 0;
         String text = rawText;
         StringBuilder sb;
@@ -386,7 +371,7 @@
         return text;
     }
 
-    private static int searchTextNameEnd(String text, int start) {
+    private static int searchTextNameEnd(final String text, final int start) {
         final int size = text.length();
         for (int pos = start; pos < size; pos++) {
             final char c = text.charAt(pos);
@@ -399,7 +384,7 @@
         return size;
     }
 
-    public static String[] parseCsvString(String rawText, KeyboardTextsSet textsSet) {
+    public static String[] parseCsvString(final String rawText, final KeyboardTextsSet textsSet) {
         final String text = resolveTextReference(rawText, textsSet);
         final int size = text.length();
         if (size == 0) {
@@ -417,7 +402,7 @@
                 // Skip empty entry.
                 if (pos - start > 0) {
                     if (list == null) {
-                        list = new ArrayList<String>();
+                        list = CollectionUtils.newArrayList();
                     }
                     list.add(text.substring(start, pos));
                 }
@@ -438,7 +423,8 @@
         return list.toArray(new String[list.size()]);
     }
 
-    public static int getIntValue(String[] moreKeys, String key, int defaultValue) {
+    public static int getIntValue(final String[] moreKeys, final String key,
+            final int defaultValue) {
         if (moreKeys == null) {
             return defaultValue;
         }
@@ -464,7 +450,7 @@
         return value;
     }
 
-    public static boolean getBooleanValue(String[] moreKeys, String key) {
+    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
         if (moreKeys == null) {
             return false;
         }
@@ -480,8 +466,8 @@
         return value;
     }
 
-    public static int toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase,
-            Locale locale) {
+    public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase,
+            final Locale locale) {
         if (!Keyboard.isLetterCode(code) || !needsToUpperCase) return code;
         final String text = new String(new int[] { code } , 0, 1);
         final String casedText = KeySpecParser.toUpperCaseOfStringForLocale(
@@ -490,8 +476,8 @@
                 ? casedText.codePointAt(0) : CODE_UNSPECIFIED;
     }
 
-    public static String toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase,
-            Locale locale) {
+    public static String toUpperCaseOfStringForLocale(final String text,
+            final boolean needsToUpperCase, final Locale locale) {
         if (text == null || !needsToUpperCase) return text;
         return text.toUpperCase(locale);
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
new file mode 100644
index 0000000..e8cacf9
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+public abstract class KeyStyle {
+    private final KeyboardTextsSet mTextsSet;
+
+    public abstract String[] getStringArray(TypedArray a, int index);
+    public abstract String getString(TypedArray a, int index);
+    public abstract int getInt(TypedArray a, int index, int defaultValue);
+    public abstract int getFlag(TypedArray a, int index);
+
+    protected KeyStyle(final KeyboardTextsSet textsSet) {
+        mTextsSet = textsSet;
+    }
+
+    protected String parseString(final TypedArray a, final int index) {
+        if (a.hasValue(index)) {
+            return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
+        }
+        return null;
+    }
+
+    protected String[] parseStringArray(final TypedArray a, final int index) {
+        if (a.hasValue(index)) {
+            return KeySpecParser.parseCsvString(a.getString(index), mTextsSet);
+        }
+        return null;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
similarity index 65%
rename from java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
rename to java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
index 80f4f25..71fd305 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
@@ -18,8 +18,9 @@
 
 import android.content.res.TypedArray;
 import android.util.Log;
+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;
 
@@ -28,120 +29,111 @@
 
 import java.util.HashMap;
 
-public class KeyStyles {
-    private static final String TAG = KeyStyles.class.getSimpleName();
+public class KeyStylesSet {
+    private static final String TAG = KeyStylesSet.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    final HashMap<String, KeyStyle> mStyles = new HashMap<String, KeyStyle>();
+    private final HashMap<String, KeyStyle> mStyles = CollectionUtils.newHashMap();
 
-    final KeyboardTextsSet mTextsSet;
+    private final KeyboardTextsSet mTextsSet;
     private final KeyStyle mEmptyKeyStyle;
     private static final String EMPTY_STYLE_NAME = "<empty>";
 
-    public KeyStyles(KeyboardTextsSet textsSet) {
+    public KeyStylesSet(final KeyboardTextsSet textsSet) {
         mTextsSet = textsSet;
-        mEmptyKeyStyle = new EmptyKeyStyle();
+        mEmptyKeyStyle = new EmptyKeyStyle(textsSet);
         mStyles.put(EMPTY_STYLE_NAME, mEmptyKeyStyle);
     }
 
-    public abstract class KeyStyle {
-        public abstract String[] getStringArray(TypedArray a, int index);
-        public abstract String getString(TypedArray a, int index);
-        public abstract int getInt(TypedArray a, int index, int defaultValue);
-        public abstract int getFlag(TypedArray a, int index);
-
-        protected String parseString(TypedArray a, int index) {
-            if (a.hasValue(index)) {
-                return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
-            }
-            return null;
+    private static class EmptyKeyStyle extends KeyStyle {
+        EmptyKeyStyle(final KeyboardTextsSet textsSet) {
+            super(textsSet);
         }
 
-        protected String[] parseStringArray(TypedArray a, int index) {
-            if (a.hasValue(index)) {
-                return KeySpecParser.parseCsvString(a.getString(index), mTextsSet);
-            }
-            return null;
-        }
-    }
-
-    class EmptyKeyStyle extends KeyStyle {
         @Override
-        public String[] getStringArray(TypedArray a, int index) {
+        public String[] getStringArray(final TypedArray a, final int index) {
             return parseStringArray(a, index);
         }
 
         @Override
-        public String getString(TypedArray a, int index) {
+        public String getString(final TypedArray a, final int index) {
             return parseString(a, index);
         }
 
         @Override
-        public int getInt(TypedArray a, int index, int defaultValue) {
+        public int getInt(final TypedArray a, final int index, final int defaultValue) {
             return a.getInt(index, defaultValue);
         }
 
         @Override
-        public int getFlag(TypedArray a, int index) {
+        public int getFlag(final TypedArray a, final int index) {
             return a.getInt(index, 0);
         }
     }
 
-    private class DeclaredKeyStyle extends KeyStyle {
+    private static class DeclaredKeyStyle extends KeyStyle {
+        private final HashMap<String, KeyStyle> mStyles;
         private final String mParentStyleName;
-        private final HashMap<Integer, Object> mStyleAttributes = new HashMap<Integer, Object>();
+        private final SparseArray<Object> mStyleAttributes = CollectionUtils.newSparseArray();
 
-        public DeclaredKeyStyle(String parentStyleName) {
+        public DeclaredKeyStyle(final String parentStyleName, final KeyboardTextsSet textsSet,
+                final HashMap<String, KeyStyle> styles) {
+            super(textsSet);
             mParentStyleName = parentStyleName;
+            mStyles = styles;
         }
 
         @Override
-        public String[] getStringArray(TypedArray a, int index) {
+        public String[] getStringArray(final TypedArray a, final int index) {
             if (a.hasValue(index)) {
                 return parseStringArray(a, index);
             }
-            if (mStyleAttributes.containsKey(index)) {
-                return (String[])mStyleAttributes.get(index);
+            final Object value = mStyleAttributes.get(index);
+            if (value != null) {
+                return (String[])value;
             }
             final KeyStyle parentStyle = mStyles.get(mParentStyleName);
             return parentStyle.getStringArray(a, index);
         }
 
         @Override
-        public String getString(TypedArray a, int index) {
+        public String getString(final TypedArray a, final int index) {
             if (a.hasValue(index)) {
                 return parseString(a, index);
             }
-            if (mStyleAttributes.containsKey(index)) {
-                return (String)mStyleAttributes.get(index);
+            final Object value = mStyleAttributes.get(index);
+            if (value != null) {
+                return (String)value;
             }
             final KeyStyle parentStyle = mStyles.get(mParentStyleName);
             return parentStyle.getString(a, index);
         }
 
         @Override
-        public int getInt(TypedArray a, int index, int defaultValue) {
+        public int getInt(final TypedArray a, final int index, final int defaultValue) {
             if (a.hasValue(index)) {
                 return a.getInt(index, defaultValue);
             }
-            if (mStyleAttributes.containsKey(index)) {
-                return (Integer)mStyleAttributes.get(index);
+            final Object value = mStyleAttributes.get(index);
+            if (value != null) {
+                return (Integer)value;
             }
             final KeyStyle parentStyle = mStyles.get(mParentStyleName);
             return parentStyle.getInt(a, index, defaultValue);
         }
 
         @Override
-        public int getFlag(TypedArray a, int index) {
-            int value = a.getInt(index, 0);
-            if (mStyleAttributes.containsKey(index)) {
-                value |= (Integer)mStyleAttributes.get(index);
+        public int getFlag(final TypedArray a, final int index) {
+            int flags = a.getInt(index, 0);
+            final Object value = mStyleAttributes.get(index);
+            if (value != null) {
+                flags |= (Integer)value;
             }
             final KeyStyle parentStyle = mStyles.get(mParentStyleName);
-            return value | parentStyle.getFlag(a, index);
+            return flags | parentStyle.getFlag(a, index);
         }
 
-        void readKeyAttributes(TypedArray keyAttr) {
+        public void readKeyAttributes(final TypedArray keyAttr) {
             // TODO: Currently not all Key attributes can be declared as style.
             readString(keyAttr, R.styleable.Keyboard_Key_code);
             readString(keyAttr, R.styleable.Keyboard_Key_altCode);
@@ -159,38 +151,38 @@
             readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
         }
 
-        private void readString(TypedArray a, int index) {
+        private void readString(final TypedArray a, final int index) {
             if (a.hasValue(index)) {
                 mStyleAttributes.put(index, parseString(a, index));
             }
         }
 
-        private void readInt(TypedArray a, int index) {
+        private void readInt(final TypedArray a, final int index) {
             if (a.hasValue(index)) {
                 mStyleAttributes.put(index, a.getInt(index, 0));
             }
         }
 
-        private void readFlag(TypedArray a, int index) {
+        private void readFlag(final TypedArray a, final int index) {
             if (a.hasValue(index)) {
                 final Integer value = (Integer)mStyleAttributes.get(index);
                 mStyleAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0));
             }
         }
 
-        private void readStringArray(TypedArray a, int index) {
+        private void readStringArray(final TypedArray a, final int index) {
             if (a.hasValue(index)) {
                 mStyleAttributes.put(index, parseStringArray(a, index));
             }
         }
     }
 
-    public void parseKeyStyleAttributes(TypedArray keyStyleAttr, TypedArray keyAttrs,
-            XmlPullParser parser) throws XmlPullParserException {
+    public void parseKeyStyleAttributes(final TypedArray keyStyleAttr, final TypedArray keyAttrs,
+            final XmlPullParser parser) throws XmlPullParserException {
         final String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName);
         if (DEBUG) {
             Log.d(TAG, String.format("<%s styleName=%s />",
-                    Keyboard.Builder.TAG_KEY_STYLE, styleName));
+                    KeyboardBuilder.TAG_KEY_STYLE, styleName));
             if (mStyles.containsKey(styleName)) {
                 Log.d(TAG, "key-style " + styleName + " is overridden at "
                         + parser.getPositionDescription());
@@ -205,12 +197,12 @@
                         "Unknown parentStyle " + parentStyleName, parser);
             }
         }
-        final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName);
+        final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName, mTextsSet, mStyles);
         style.readKeyAttributes(keyAttrs);
         mStyles.put(styleName, style);
     }
 
-    public KeyStyle getKeyStyle(TypedArray keyAttr, XmlPullParser parser)
+    public KeyStyle getKeyStyle(final TypedArray keyAttr, final XmlPullParser parser)
             throws XmlParseUtils.ParseException {
         if (!keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
             return mEmptyKeyStyle;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
new file mode 100644
index 0000000..04cc152
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
@@ -0,0 +1,130 @@
+/*
+ * 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.Typeface;
+import android.util.SparseIntArray;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
+
+public class KeyVisualAttributes {
+    public final Typeface mTypeface;
+
+    public final float mLetterRatio;
+    public final int mLetterSize;
+    public final float mLabelRatio;
+    public final int mLabelSize;
+    public final float mLargeLetterRatio;
+    public final float mLargeLabelRatio;
+    public final float mHintLetterRatio;
+    public final float mShiftedLetterHintRatio;
+    public final float mHintLabelRatio;
+    public final float mPreviewTextRatio;
+
+    public final int mTextColor;
+    public final int mTextInactivatedColor;
+    public final int mTextShadowColor;
+    public final int mHintLetterColor;
+    public final int mHintLabelColor;
+    public final int mShiftedLetterHintInactivatedColor;
+    public final int mShiftedLetterHintActivatedColor;
+    public final int mPreviewTextColor;
+
+    private static final int[] VISUAL_ATTRIBUTE_IDS = {
+        R.styleable.Keyboard_Key_keyTypeface,
+        R.styleable.Keyboard_Key_keyLetterSize,
+        R.styleable.Keyboard_Key_keyLabelSize,
+        R.styleable.Keyboard_Key_keyLargeLetterRatio,
+        R.styleable.Keyboard_Key_keyLargeLabelRatio,
+        R.styleable.Keyboard_Key_keyHintLetterRatio,
+        R.styleable.Keyboard_Key_keyShiftedLetterHintRatio,
+        R.styleable.Keyboard_Key_keyHintLabelRatio,
+        R.styleable.Keyboard_Key_keyPreviewTextRatio,
+        R.styleable.Keyboard_Key_keyTextColor,
+        R.styleable.Keyboard_Key_keyTextInactivatedColor,
+        R.styleable.Keyboard_Key_keyTextShadowColor,
+        R.styleable.Keyboard_Key_keyHintLetterColor,
+        R.styleable.Keyboard_Key_keyHintLabelColor,
+        R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor,
+        R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor,
+        R.styleable.Keyboard_Key_keyPreviewTextColor,
+    };
+    private static final SparseIntArray sVisualAttributeIds = new SparseIntArray();
+    private static final int ATTR_DEFINED = 1;
+    private static final int ATTR_NOT_FOUND = 0;
+    static {
+        for (final int attrId : VISUAL_ATTRIBUTE_IDS) {
+            sVisualAttributeIds.put(attrId, ATTR_DEFINED);
+        }
+    }
+
+    public static KeyVisualAttributes newInstance(final TypedArray keyAttr) {
+        final int indexCount = keyAttr.getIndexCount();
+        for (int i = 0; i < indexCount; i++) {
+            final int attrId = keyAttr.getIndex(i);
+            if (sVisualAttributeIds.get(attrId, ATTR_NOT_FOUND) == ATTR_NOT_FOUND) {
+                continue;
+            }
+            return new KeyVisualAttributes(keyAttr);
+        }
+        return null;
+    }
+
+    private KeyVisualAttributes(final TypedArray keyAttr) {
+        if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyTypeface)) {
+            mTypeface = Typeface.defaultFromStyle(
+                    keyAttr.getInt(R.styleable.Keyboard_Key_keyTypeface, Typeface.NORMAL));
+        } else {
+            mTypeface = null;
+        }
+
+        mLetterRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyLetterSize);
+        mLetterSize = ResourceUtils.getDimensionPixelSize(keyAttr,
+                R.styleable.Keyboard_Key_keyLetterSize);
+        mLabelRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyLabelSize);
+        mLabelSize = ResourceUtils.getDimensionPixelSize(keyAttr,
+                R.styleable.Keyboard_Key_keyLabelSize);
+        mLargeLetterRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyLargeLetterRatio);
+        mLargeLabelRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyLargeLabelRatio);
+        mHintLetterRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyHintLetterRatio);
+        mShiftedLetterHintRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyShiftedLetterHintRatio);
+        mHintLabelRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyHintLabelRatio);
+        mPreviewTextRatio = ResourceUtils.getFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyPreviewTextRatio);
+
+        mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0);
+        mTextInactivatedColor = keyAttr.getColor(
+                R.styleable.Keyboard_Key_keyTextInactivatedColor, 0);
+        mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
+        mHintLetterColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
+        mHintLabelColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
+        mShiftedLetterHintInactivatedColor = keyAttr.getColor(
+                R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0);
+        mShiftedLetterHintActivatedColor = keyAttr.getColor(
+                R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
+        mPreviewTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0);
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
new file mode 100644
index 0000000..31c7cb5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -0,0 +1,814 @@
+/*
+ * 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.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.InflateException;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
+import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.SubtypeLocale;
+import com.android.inputmethod.latin.XmlParseUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Keyboard Building helper.
+ *
+ * This class parses Keyboard XML file and eventually build a Keyboard.
+ * The Keyboard XML file looks like:
+ * <pre>
+ *   &lt;!-- xml/keyboard.xml --&gt;
+ *   &lt;Keyboard keyboard_attributes*&gt;
+ *     &lt;!-- Keyboard Content --&gt;
+ *     &lt;Row row_attributes*&gt;
+ *       &lt;!-- Row Content --&gt;
+ *       &lt;Key key_attributes* /&gt;
+ *       &lt;Spacer horizontalGap="32.0dp" /&gt;
+ *       &lt;include keyboardLayout="@xml/other_keys"&gt;
+ *       ...
+ *     &lt;/Row&gt;
+ *     &lt;include keyboardLayout="@xml/other_rows"&gt;
+ *     ...
+ *   &lt;/Keyboard&gt;
+ * </pre>
+ * The XML file which is included in other file must have &lt;merge&gt; as root element,
+ * such as:
+ * <pre>
+ *   &lt;!-- xml/other_keys.xml --&gt;
+ *   &lt;merge&gt;
+ *     &lt;Key key_attributes* /&gt;
+ *     ...
+ *   &lt;/merge&gt;
+ * </pre>
+ * and
+ * <pre>
+ *   &lt;!-- xml/other_rows.xml --&gt;
+ *   &lt;merge&gt;
+ *     &lt;Row row_attributes*&gt;
+ *       &lt;Key key_attributes* /&gt;
+ *     &lt;/Row&gt;
+ *     ...
+ *   &lt;/merge&gt;
+ * </pre>
+ * You can also use switch-case-default tags to select Rows and Keys.
+ * <pre>
+ *   &lt;switch&gt;
+ *     &lt;case case_attribute*&gt;
+ *       &lt;!-- Any valid tags at switch position --&gt;
+ *     &lt;/case&gt;
+ *     ...
+ *     &lt;default&gt;
+ *       &lt;!-- Any valid tags at switch position --&gt;
+ *     &lt;/default&gt;
+ *   &lt;/switch&gt;
+ * </pre>
+ * You can declare Key style and specify styles within Key tags.
+ * <pre>
+ *     &lt;switch&gt;
+ *       &lt;case mode="email"&gt;
+ *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
+ *           keyLabel=".com"
+ *         /&gt;
+ *       &lt;/case&gt;
+ *       &lt;case mode="url"&gt;
+ *         &lt;key-style styleName="f1-key" parentStyle="modifier-key"
+ *           keyLabel="http://"
+ *         /&gt;
+ *       &lt;/case&gt;
+ *     &lt;/switch&gt;
+ *     ...
+ *     &lt;Key keyStyle="shift-key" ... /&gt;
+ * </pre>
+ */
+
+public class KeyboardBuilder<KP extends KeyboardParams> {
+    private static final String BUILDER_TAG = "Keyboard.Builder";
+    private static final boolean DEBUG = false;
+
+    // Keyboard XML Tags
+    private static final String TAG_KEYBOARD = "Keyboard";
+    private static final String TAG_ROW = "Row";
+    private static final String TAG_KEY = "Key";
+    private static final String TAG_SPACER = "Spacer";
+    private static final String TAG_INCLUDE = "include";
+    private static final String TAG_MERGE = "merge";
+    private static final String TAG_SWITCH = "switch";
+    private static final String TAG_CASE = "case";
+    private static final String TAG_DEFAULT = "default";
+    public static final String TAG_KEY_STYLE = "key-style";
+
+    private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
+    private static final int DEFAULT_KEYBOARD_ROWS = 4;
+
+    protected final KP mParams;
+    protected final Context mContext;
+    protected final Resources mResources;
+    private final DisplayMetrics mDisplayMetrics;
+
+    private int mCurrentY = 0;
+    private KeyboardRow mCurrentRow = null;
+    private boolean mLeftEdge;
+    private boolean mTopEdge;
+    private Key mRightEdgeKey = null;
+
+    public KeyboardBuilder(final Context context, final KP params) {
+        mContext = context;
+        final Resources res = context.getResources();
+        mResources = res;
+        mDisplayMetrics = res.getDisplayMetrics();
+
+        mParams = params;
+
+        params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
+        params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
+    }
+
+    public void setAutoGenerate(final KeysCache keysCache) {
+        mParams.mKeysCache = keysCache;
+    }
+
+    public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
+        mParams.mId = id;
+        final XmlResourceParser parser = mResources.getXml(xmlId);
+        try {
+            parseKeyboard(parser);
+        } catch (XmlPullParserException e) {
+            Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
+            throw new IllegalArgumentException(e);
+        } catch (IOException e) {
+            Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
+            throw new RuntimeException(e);
+        } finally {
+            parser.close();
+        }
+        return this;
+    }
+
+    // TODO: Remove this method.
+    public void setTouchPositionCorrectionEnabled(final boolean enabled) {
+        mParams.mTouchPositionCorrection.setEnabled(enabled);
+    }
+
+    public void setProximityCharsCorrectionEnabled(final boolean enabled) {
+        mParams.mProximityCharsCorrectionEnabled = enabled;
+    }
+
+    public Keyboard build() {
+        return new Keyboard(mParams);
+    }
+
+    private int mIndent;
+    private static final String SPACES = "                                             ";
+
+    private static String spaces(final int count) {
+        return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
+    }
+
+    private void startTag(final String format, final Object ... args) {
+        Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
+    }
+
+    private void endTag(final String format, final Object ... args) {
+        Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
+    }
+
+    private void startEndTag(final String format, final Object ... args) {
+        Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
+        mIndent--;
+    }
+
+    private void parseKeyboard(final XmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
+        int event;
+        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (event == XmlPullParser.START_TAG) {
+                final String tag = parser.getName();
+                if (TAG_KEYBOARD.equals(tag)) {
+                    parseKeyboardAttributes(parser);
+                    startKeyboard();
+                    parseKeyboardContent(parser, false);
+                    break;
+                } else {
+                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
+                }
+            }
+        }
+    }
+
+    private void parseKeyboardAttributes(final XmlPullParser parser) {
+        final int displayWidth = mDisplayMetrics.widthPixels;
+        final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
+                Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
+                R.style.Keyboard);
+        final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+                R.styleable.Keyboard_Key);
+        try {
+            final int displayHeight = mDisplayMetrics.heightPixels;
+            final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
+                    mResources, R.array.keyboard_heights, null);
+            final float keyboardHeight;
+            if (keyboardHeightString != null) {
+                keyboardHeight = Float.parseFloat(keyboardHeightString)
+                        * mDisplayMetrics.density;
+            } else {
+                keyboardHeight = keyboardAttr.getDimension(
+                        R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
+            }
+            final float maxKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                    R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
+            float minKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                    R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
+            if (minKeyboardHeight < 0) {
+                // Specified fraction was negative, so it should be calculated against display
+                // width.
+                minKeyboardHeight = -ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                        R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
+            }
+            final KeyboardParams params = mParams;
+            // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+            // minKeyboardHeight.
+            params.mOccupiedHeight = (int)Math.max(
+                    Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+            params.mOccupiedWidth = params.mId.mWidth;
+            params.mTopPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                    R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
+            params.mBottomPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                    R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
+            params.mHorizontalEdgesPadding = (int)ResourceUtils.getDimensionOrFraction(
+                    keyboardAttr,
+                    R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
+                    mParams.mOccupiedWidth, 0);
+
+            params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
+                    - params.mHorizontalCenterPadding;
+            params.mDefaultKeyWidth = (int)ResourceUtils.getDimensionOrFraction(keyAttr,
+                    R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
+                    params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
+            params.mHorizontalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                    R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
+            params.mVerticalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                    R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
+            params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
+                    - params.mBottomPadding + params.mVerticalGap;
+            params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                    R.styleable.Keyboard_rowHeight, params.mBaseHeight,
+                    params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
+
+            params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
+
+            params.mMoreKeysTemplate = keyboardAttr.getResourceId(
+                    R.styleable.Keyboard_moreKeysTemplate, 0);
+            params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
+                    R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
+
+            params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
+            params.mIconsSet.loadIcons(keyboardAttr);
+            final String language = params.mId.mLocale.getLanguage();
+            params.mCodesSet.setLanguage(language);
+            params.mTextsSet.setLanguage(language);
+            final RunInLocale<Void> job = new RunInLocale<Void>() {
+                @Override
+                protected Void job(Resources res) {
+                    params.mTextsSet.loadStringResources(mContext);
+                    return null;
+                }
+            };
+            // Null means the current system locale.
+            final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
+                    ? null : params.mId.mLocale;
+            job.runInLocale(mResources, locale);
+
+            final int resourceId = keyboardAttr.getResourceId(
+                    R.styleable.Keyboard_touchPositionCorrectionData, 0);
+            params.mTouchPositionCorrection.setEnabled(resourceId != 0);
+            if (resourceId != 0) {
+                final String[] data = mResources.getStringArray(resourceId);
+                params.mTouchPositionCorrection.load(data);
+            }
+        } finally {
+            keyAttr.recycle();
+            keyboardAttr.recycle();
+        }
+    }
+
+    private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
+            throws XmlPullParserException, IOException {
+        int event;
+        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (event == XmlPullParser.START_TAG) {
+                final String tag = parser.getName();
+                if (TAG_ROW.equals(tag)) {
+                    final KeyboardRow row = parseRowAttributes(parser);
+                    if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
+                    if (!skip) {
+                        startRow(row);
+                    }
+                    parseRowContent(parser, row, skip);
+                } else if (TAG_INCLUDE.equals(tag)) {
+                    parseIncludeKeyboardContent(parser, skip);
+                } else if (TAG_SWITCH.equals(tag)) {
+                    parseSwitchKeyboardContent(parser, skip);
+                } else if (TAG_KEY_STYLE.equals(tag)) {
+                    parseKeyStyle(parser, skip);
+                } else {
+                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
+                }
+            } else if (event == XmlPullParser.END_TAG) {
+                final String tag = parser.getName();
+                if (DEBUG) endTag("</%s>", tag);
+                if (TAG_KEYBOARD.equals(tag)) {
+                    endKeyboard();
+                    break;
+                } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
+                        || TAG_MERGE.equals(tag)) {
+                    break;
+                } else {
+                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW);
+                }
+            }
+        }
+    }
+
+    private KeyboardRow parseRowAttributes(final XmlPullParser parser)
+            throws XmlPullParserException {
+        final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+                R.styleable.Keyboard);
+        try {
+            if (a.hasValue(R.styleable.Keyboard_horizontalGap)) {
+                throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
+            }
+            if (a.hasValue(R.styleable.Keyboard_verticalGap)) {
+                throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
+            }
+            return new KeyboardRow(mResources, mParams, parser, mCurrentY);
+        } finally {
+            a.recycle();
+        }
+    }
+
+    private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
+            final boolean skip) throws XmlPullParserException, IOException {
+        int event;
+        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (event == XmlPullParser.START_TAG) {
+                final String tag = parser.getName();
+                if (TAG_KEY.equals(tag)) {
+                    parseKey(parser, row, skip);
+                } else if (TAG_SPACER.equals(tag)) {
+                    parseSpacer(parser, row, skip);
+                } else if (TAG_INCLUDE.equals(tag)) {
+                    parseIncludeRowContent(parser, row, skip);
+                } else if (TAG_SWITCH.equals(tag)) {
+                    parseSwitchRowContent(parser, row, skip);
+                } else if (TAG_KEY_STYLE.equals(tag)) {
+                    parseKeyStyle(parser, skip);
+                } else {
+                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
+                }
+            } else if (event == XmlPullParser.END_TAG) {
+                final String tag = parser.getName();
+                if (DEBUG) endTag("</%s>", tag);
+                if (TAG_ROW.equals(tag)) {
+                    if (!skip) {
+                        endRow(row);
+                    }
+                    break;
+                } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
+                        || TAG_MERGE.equals(tag)) {
+                    break;
+                } else {
+                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
+                }
+            }
+        }
+    }
+
+    private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+            throws XmlPullParserException, IOException {
+        if (skip) {
+            XmlParseUtils.checkEndTag(TAG_KEY, parser);
+            if (DEBUG) {
+                startEndTag("<%s /> skipped", TAG_KEY);
+            }
+        } else {
+            final Key key = new Key(mResources, mParams, row, parser);
+            if (DEBUG) {
+                startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
+                        (key.isEnabled() ? "" : " disabled"), key,
+                        Arrays.toString(key.mMoreKeys));
+            }
+            XmlParseUtils.checkEndTag(TAG_KEY, parser);
+            endKey(key);
+        }
+    }
+
+    private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+            throws XmlPullParserException, IOException {
+        if (skip) {
+            XmlParseUtils.checkEndTag(TAG_SPACER, parser);
+            if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
+        } else {
+            final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
+            if (DEBUG) startEndTag("<%s />", TAG_SPACER);
+            XmlParseUtils.checkEndTag(TAG_SPACER, parser);
+            endKey(spacer);
+        }
+    }
+
+    private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
+            throws XmlPullParserException, IOException {
+        parseIncludeInternal(parser, null, skip);
+    }
+
+    private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
+            final boolean skip) throws XmlPullParserException, IOException {
+        parseIncludeInternal(parser, row, skip);
+    }
+
+    private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
+            final boolean skip) throws XmlPullParserException, IOException {
+        if (skip) {
+            XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
+            if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
+        } else {
+            final AttributeSet attr = Xml.asAttributeSet(parser);
+            final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
+                    R.styleable.Keyboard_Include);
+            final TypedArray keyAttr = mResources.obtainAttributes(attr,
+                    R.styleable.Keyboard_Key);
+            int keyboardLayout = 0;
+            float savedDefaultKeyWidth = 0;
+            int savedDefaultKeyLabelFlags = 0;
+            int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
+            try {
+                XmlParseUtils.checkAttributeExists(keyboardAttr,
+                        R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
+                        TAG_INCLUDE, parser);
+                keyboardLayout = keyboardAttr.getResourceId(
+                        R.styleable.Keyboard_Include_keyboardLayout, 0);
+                if (row != null) {
+                    if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
+                        // Override current x coordinate.
+                        row.setXPos(row.getKeyX(keyAttr));
+                    }
+                    // TODO: Remove this if-clause and do the same as backgroundType below.
+                    savedDefaultKeyWidth = row.getDefaultKeyWidth();
+                    if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
+                        // Override default key width.
+                        row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
+                    }
+                    savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
+                    // Bitwise-or default keyLabelFlag if exists.
+                    row.setDefaultKeyLabelFlags(keyAttr.getInt(
+                            R.styleable.Keyboard_Key_keyLabelFlags, 0)
+                            | savedDefaultKeyLabelFlags);
+                    savedDefaultBackgroundType = row.getDefaultBackgroundType();
+                    // Override default backgroundType if exists.
+                    row.setDefaultBackgroundType(keyAttr.getInt(
+                            R.styleable.Keyboard_Key_backgroundType,
+                            savedDefaultBackgroundType));
+                }
+            } finally {
+                keyboardAttr.recycle();
+                keyAttr.recycle();
+            }
+
+            XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
+            if (DEBUG) {
+                startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
+                        mResources.getResourceEntryName(keyboardLayout));
+            }
+            final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
+            try {
+                parseMerge(parserForInclude, row, skip);
+            } finally {
+                if (row != null) {
+                    // Restore default keyWidth, keyLabelFlags, and backgroundType.
+                    row.setDefaultKeyWidth(savedDefaultKeyWidth);
+                    row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
+                    row.setDefaultBackgroundType(savedDefaultBackgroundType);
+                }
+                parserForInclude.close();
+            }
+        }
+    }
+
+    private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+            throws XmlPullParserException, IOException {
+        if (DEBUG) startTag("<%s>", TAG_MERGE);
+        int event;
+        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (event == XmlPullParser.START_TAG) {
+                final String tag = parser.getName();
+                if (TAG_MERGE.equals(tag)) {
+                    if (row == null) {
+                        parseKeyboardContent(parser, skip);
+                    } else {
+                        parseRowContent(parser, row, skip);
+                    }
+                    break;
+                } else {
+                    throw new XmlParseUtils.ParseException(
+                            "Included keyboard layout must have <merge> root element", parser);
+                }
+            }
+        }
+    }
+
+    private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
+            throws XmlPullParserException, IOException {
+        parseSwitchInternal(parser, null, skip);
+    }
+
+    private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
+            final boolean skip) throws XmlPullParserException, IOException {
+        parseSwitchInternal(parser, row, skip);
+    }
+
+    private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row,
+            final boolean skip) throws XmlPullParserException, IOException {
+        if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
+        boolean selected = false;
+        int event;
+        while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (event == XmlPullParser.START_TAG) {
+                final String tag = parser.getName();
+                if (TAG_CASE.equals(tag)) {
+                    selected |= parseCase(parser, row, selected ? true : skip);
+                } else if (TAG_DEFAULT.equals(tag)) {
+                    selected |= parseDefault(parser, row, selected ? true : skip);
+                } else {
+                    throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
+                }
+            } else if (event == XmlPullParser.END_TAG) {
+                final String tag = parser.getName();
+                if (TAG_SWITCH.equals(tag)) {
+                    if (DEBUG) endTag("</%s>", TAG_SWITCH);
+                    break;
+                } else {
+                    throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
+                }
+            }
+        }
+    }
+
+    private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+            throws XmlPullParserException, IOException {
+        final boolean selected = parseCaseCondition(parser);
+        if (row == null) {
+            // Processing Rows.
+            parseKeyboardContent(parser, selected ? skip : true);
+        } else {
+            // Processing Keys.
+            parseRowContent(parser, row, selected ? skip : true);
+        }
+        return selected;
+    }
+
+    private boolean parseCaseCondition(final XmlPullParser parser) {
+        final KeyboardId id = mParams.mId;
+        if (id == null) {
+            return true;
+        }
+        final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+                R.styleable.Keyboard_Case);
+        try {
+            final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
+                    R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
+                    KeyboardId.elementIdToName(id.mElementId));
+            final boolean modeMatched = matchTypedValue(a,
+                    R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
+            final boolean navigateNextMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
+            final boolean navigatePreviousMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
+            final boolean passwordInputMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
+            final boolean clobberSettingsKeyMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
+            final boolean shortcutKeyEnabledMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
+            final boolean hasShortcutKeyMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
+            final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+                    id.mLanguageSwitchKeyEnabled);
+            final boolean isMultiLineMatched = matchBoolean(a,
+                    R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
+            final boolean imeActionMatched = matchInteger(a,
+                    R.styleable.Keyboard_Case_imeAction, id.imeAction());
+            final boolean localeCodeMatched = matchString(a,
+                    R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
+            final boolean languageCodeMatched = matchString(a,
+                    R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
+            final boolean countryCodeMatched = matchString(a,
+                    R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
+            final boolean selected = keyboardLayoutSetElementMatched && modeMatched
+                    && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
+                    && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
+                    && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched
+                    && isMultiLineMatched && imeActionMatched && localeCodeMatched
+                    && languageCodeMatched && countryCodeMatched;
+
+            if (DEBUG) {
+                startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
+                        textAttr(a.getString(
+                                R.styleable.Keyboard_Case_keyboardLayoutSetElement),
+                                "keyboardLayoutSetElement"),
+                        textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
+                        textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
+                                "imeAction"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
+                                "navigateNext"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
+                                "navigatePrevious"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
+                                "clobberSettingsKey"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
+                                "passwordInput"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
+                                "shortcutKeyEnabled"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
+                                "hasShortcutKey"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+                                "languageSwitchKeyEnabled"),
+                        booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
+                                "isMultiLine"),
+                        textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
+                                "localeCode"),
+                        textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
+                                "languageCode"),
+                        textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
+                                "countryCode"),
+                        selected ? "" : " skipped");
+            }
+
+            return selected;
+        } finally {
+            a.recycle();
+        }
+    }
+
+    private static boolean matchInteger(final TypedArray a, final int index, final int value) {
+        // If <case> does not have "index" attribute, that means this <case> is wild-card for
+        // the attribute.
+        return !a.hasValue(index) || a.getInt(index, 0) == value;
+    }
+
+    private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
+        // If <case> does not have "index" attribute, that means this <case> is wild-card for
+        // the attribute.
+        return !a.hasValue(index) || a.getBoolean(index, false) == value;
+    }
+
+    private static boolean matchString(final TypedArray a, final int index, final String value) {
+        // If <case> does not have "index" attribute, that means this <case> is wild-card for
+        // the attribute.
+        return !a.hasValue(index)
+                || StringUtils.containsInArray(value, a.getString(index).split("\\|"));
+    }
+
+    private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
+            final String strValue) {
+        // 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) {
+            return true;
+        }
+        if (ResourceUtils.isIntegerValue(v)) {
+            return intValue == a.getInt(index, 0);
+        } else if (ResourceUtils.isStringValue(v)) {
+            return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
+        }
+        return false;
+    }
+
+    private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row,
+            final boolean skip) throws XmlPullParserException, IOException {
+        if (DEBUG) startTag("<%s>", TAG_DEFAULT);
+        if (row == null) {
+            parseKeyboardContent(parser, skip);
+        } else {
+            parseRowContent(parser, row, skip);
+        }
+        return true;
+    }
+
+    private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
+            throws XmlPullParserException, IOException {
+        TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+                R.styleable.Keyboard_KeyStyle);
+        TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+                R.styleable.Keyboard_Key);
+        try {
+            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" : "");
+            }
+            if (!skip) {
+                mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
+            }
+        } finally {
+            keyStyleAttr.recycle();
+            keyAttrs.recycle();
+        }
+        XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
+    }
+
+    private void startKeyboard() {
+        mCurrentY += mParams.mTopPadding;
+        mTopEdge = true;
+    }
+
+    private void startRow(final KeyboardRow row) {
+        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+        mCurrentRow = row;
+        mLeftEdge = true;
+        mRightEdgeKey = null;
+    }
+
+    private void endRow(final KeyboardRow row) {
+        if (mCurrentRow == null) {
+            throw new InflateException("orphan end row tag");
+        }
+        if (mRightEdgeKey != null) {
+            mRightEdgeKey.markAsRightEdge(mParams);
+            mRightEdgeKey = null;
+        }
+        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+        mCurrentY += row.mRowHeight;
+        mCurrentRow = null;
+        mTopEdge = false;
+    }
+
+    private void endKey(final Key key) {
+        mParams.onAddKey(key);
+        if (mLeftEdge) {
+            key.markAsLeftEdge(mParams);
+            mLeftEdge = false;
+        }
+        if (mTopEdge) {
+            key.markAsTopEdge(mParams);
+        }
+        mRightEdgeKey = key;
+    }
+
+    private void endKeyboard() {
+        // nothing to do here.
+    }
+
+    private void addEdgeSpace(final float width, final KeyboardRow row) {
+        row.advanceXPos(width);
+        mLeftEdge = false;
+        mRightEdgeKey = null;
+    }
+
+    private static String textAttr(final String value, final String name) {
+        return value != null ? String.format(" %s=%s", name, value) : "";
+    }
+
+    private static String booleanAttr(final TypedArray a, final int index, final String name) {
+        return a.hasValue(index)
+                ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index 67cb74f..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;
 
@@ -52,6 +52,7 @@
         "key_action_next",
         "key_action_previous",
         "key_language_switch",
+        "key_research",
         "key_unspecified",
         "key_left_parenthesis",
         "key_right_parenthesis",
@@ -86,6 +87,7 @@
         Keyboard.CODE_ACTION_NEXT,
         Keyboard.CODE_ACTION_PREVIOUS,
         Keyboard.CODE_LANGUAGE_SWITCH,
+        Keyboard.CODE_RESEARCH,
         Keyboard.CODE_UNSPECIFIED,
         CODE_LEFT_PARENTHESIS,
         CODE_RIGHT_PARENTHESIS,
@@ -112,6 +114,7 @@
         DEFAULT[11],
         DEFAULT[12],
         DEFAULT[13],
+        DEFAULT[14],
         CODE_RIGHT_PARENTHESIS,
         CODE_LEFT_PARENTHESIS,
         CODE_GREATER_THAN_SIGN,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 540e63b..4a98a36 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -20,7 +20,9 @@
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
 import android.util.Log;
+import android.util.SparseIntArray;
 
+import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 
 import java.util.HashMap;
@@ -31,11 +33,10 @@
     public static final int ICON_UNDEFINED = 0;
     private static final int ATTR_UNDEFINED = 0;
 
-    private static final HashMap<Integer, Integer> ATTR_ID_TO_ICON_ID
-            = new HashMap<Integer, Integer>();
+    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,
@@ -76,7 +77,9 @@
     }
 
     public void loadIcons(final TypedArray keyboardAttrs) {
-        for (final Integer attrId : ATTR_ID_TO_ICON_ID.keySet()) {
+        final int size = ATTR_ID_TO_ICON_ID.size();
+        for (int index = 0; index < size; index++) {
+            final int attrId = ATTR_ID_TO_ICON_ID.keyAt(index);
             try {
                 final Drawable icon = keyboardAttrs.getDrawable(attrId);
                 setDefaultBounds(icon);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
new file mode 100644
index 0000000..e6fe50e
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
@@ -0,0 +1,137 @@
+/*
+ * 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.util.SparseIntArray;
+
+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 java.util.ArrayList;
+import java.util.TreeSet;
+
+public class KeyboardParams {
+    public KeyboardId mId;
+    public int mThemeId;
+
+    /** Total height and width of the keyboard, including the paddings and keys */
+    public int mOccupiedHeight;
+    public int mOccupiedWidth;
+
+    /** Base height and width of the keyboard used to calculate rows' or keys' heights and
+     *  widths
+     */
+    public int mBaseHeight;
+    public int mBaseWidth;
+
+    public int mTopPadding;
+    public int mBottomPadding;
+    public int mHorizontalEdgesPadding;
+    public int mHorizontalCenterPadding;
+
+    public KeyVisualAttributes mKeyVisualAttributes;
+
+    public int mDefaultRowHeight;
+    public int mDefaultKeyWidth;
+    public int mHorizontalGap;
+    public int mVerticalGap;
+
+    public int mMoreKeysTemplate;
+    public int mMaxMoreKeysKeyboardColumn;
+
+    public int GRID_WIDTH;
+    public int GRID_HEIGHT;
+
+    public final TreeSet<Key> mKeys = CollectionUtils.newTreeSet(); // ordered set
+    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();
+    public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);
+
+    public KeysCache mKeysCache;
+
+    public int mMostCommonKeyHeight = 0;
+    public int mMostCommonKeyWidth = 0;
+
+    public boolean mProximityCharsCorrectionEnabled;
+
+    public final TouchPositionCorrection mTouchPositionCorrection =
+            new TouchPositionCorrection();
+
+    protected void clearKeys() {
+        mKeys.clear();
+        mShiftKeys.clear();
+        clearHistogram();
+    }
+
+    public void onAddKey(final Key newKey) {
+        final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey;
+        final boolean zeroWidthSpacer = key.isSpacer() && key.mWidth == 0;
+        if (!zeroWidthSpacer) {
+            mKeys.add(key);
+            updateHistogram(key);
+        }
+        if (key.mCode == Keyboard.CODE_SHIFT) {
+            mShiftKeys.add(key);
+        }
+        if (key.altCodeWhileTyping()) {
+            mAltCodeKeysWhileTyping.add(key);
+        }
+    }
+
+    private int mMaxHeightCount = 0;
+    private int mMaxWidthCount = 0;
+    private final SparseIntArray mHeightHistogram = new SparseIntArray();
+    private final SparseIntArray mWidthHistogram = new SparseIntArray();
+
+    private void clearHistogram() {
+        mMostCommonKeyHeight = 0;
+        mMaxHeightCount = 0;
+        mHeightHistogram.clear();
+
+        mMaxWidthCount = 0;
+        mMostCommonKeyWidth = 0;
+        mWidthHistogram.clear();
+    }
+
+    private static int updateHistogramCounter(final SparseIntArray histogram, final int key) {
+        final int index = histogram.indexOfKey(key);
+        final int count = (index >= 0 ? histogram.get(key) : 0) + 1;
+        histogram.put(key, count);
+        return count;
+    }
+
+    private void updateHistogram(final Key key) {
+        final int height = key.mHeight + mVerticalGap;
+        final int heightCount = updateHistogramCounter(mHeightHistogram, height);
+        if (heightCount > mMaxHeightCount) {
+            mMaxHeightCount = heightCount;
+            mMostCommonKeyHeight = height;
+        }
+
+        final int width = key.mWidth + mHorizontalGap;
+        final int widthCount = updateHistogramCounter(mWidthHistogram, width);
+        if (widthCount > mMaxWidthCount) {
+            mMaxWidthCount = widthCount;
+            mMostCommonKeyWidth = width;
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
new file mode 100644
index 0000000..eb17b0e
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
@@ -0,0 +1,154 @@
+/*
+ * 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.Resources;
+import android.content.res.TypedArray;
+import android.util.Xml;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
+ * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
+ * defines.
+ */
+public class KeyboardRow {
+    // keyWidth enum constants
+    private static final int KEYWIDTH_NOT_ENUM = 0;
+    private static final int KEYWIDTH_FILL_RIGHT = -1;
+
+    private final KeyboardParams mParams;
+    /** Default width of a key in this row. */
+    private float mDefaultKeyWidth;
+    /** Default height of a key in this row. */
+    public final int mRowHeight;
+    /** Default keyLabelFlags in this row. */
+    private int mDefaultKeyLabelFlags;
+    /** Default backgroundType for this row */
+    private int mDefaultBackgroundType;
+
+    private final int mCurrentY;
+    // Will be updated by {@link Key}'s constructor.
+    private float mCurrentX;
+
+    public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser,
+            final int y) {
+        mParams = params;
+        TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+                R.styleable.Keyboard);
+        mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+                R.styleable.Keyboard_rowHeight,
+                params.mBaseHeight, params.mDefaultRowHeight);
+        keyboardAttr.recycle();
+        TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+                R.styleable.Keyboard_Key);
+        mDefaultKeyWidth = ResourceUtils.getDimensionOrFraction(keyAttr,
+                R.styleable.Keyboard_Key_keyWidth,
+                params.mBaseWidth, params.mDefaultKeyWidth);
+        mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+                Key.BACKGROUND_TYPE_NORMAL);
+        keyAttr.recycle();
+
+        // TODO: Initialize this with <Row> attribute as backgroundType is done.
+        mDefaultKeyLabelFlags = 0;
+        mCurrentY = y;
+        mCurrentX = 0.0f;
+    }
+
+    public float getDefaultKeyWidth() {
+        return mDefaultKeyWidth;
+    }
+
+    public void setDefaultKeyWidth(final float defaultKeyWidth) {
+        mDefaultKeyWidth = defaultKeyWidth;
+    }
+
+    public int getDefaultKeyLabelFlags() {
+        return mDefaultKeyLabelFlags;
+    }
+
+    public void setDefaultKeyLabelFlags(final int keyLabelFlags) {
+        mDefaultKeyLabelFlags = keyLabelFlags;
+    }
+
+    public int getDefaultBackgroundType() {
+        return mDefaultBackgroundType;
+    }
+
+    public void setDefaultBackgroundType(final int backgroundType) {
+        mDefaultBackgroundType = backgroundType;
+    }
+
+    public void setXPos(final float keyXPos) {
+        mCurrentX = keyXPos;
+    }
+
+    public void advanceXPos(final float width) {
+        mCurrentX += width;
+    }
+
+    public int getKeyY() {
+        return mCurrentY;
+    }
+
+    public float getKeyX(final TypedArray keyAttr) {
+        final int keyboardRightEdge = mParams.mOccupiedWidth
+                - mParams.mHorizontalEdgesPadding;
+        if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
+            final float keyXPos = ResourceUtils.getDimensionOrFraction(keyAttr,
+                    R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
+            if (keyXPos < 0) {
+                // If keyXPos is negative, the actual x-coordinate will be
+                // keyboardWidth + keyXPos.
+                // keyXPos shouldn't be less than mCurrentX because drawable area for this
+                // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
+                // its left hand side.
+                return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
+            } else {
+                return keyXPos + mParams.mHorizontalEdgesPadding;
+            }
+        }
+        return mCurrentX;
+    }
+
+    public float getKeyWidth(final TypedArray keyAttr) {
+        return getKeyWidth(keyAttr, mCurrentX);
+    }
+
+    public float getKeyWidth(final TypedArray keyAttr, final float keyXPos) {
+        final int widthType = ResourceUtils.getEnumValue(keyAttr,
+                R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
+        switch (widthType) {
+        case KEYWIDTH_FILL_RIGHT:
+            final int keyboardRightEdge =
+                    mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
+            // If keyWidth is fillRight, the actual key width will be determined to fill
+            // out the area up to the right edge of the keyboard.
+            return keyboardRightEdge - keyXPos;
+        default: // KEYWIDTH_NOT_ENUM
+            return ResourceUtils.getDimensionOrFraction(keyAttr,
+                    R.styleable.Keyboard_Key_keyWidth,
+                    mParams.mBaseWidth, mDefaultKeyWidth);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 43ffb85..4ab6832 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -21,8 +21,6 @@
 
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.ResearchLogger;
-import com.android.inputmethod.latin.define.ProductionFlag;
 
 /**
  * Keyboard state machine.
@@ -305,9 +303,6 @@
             Log.d(TAG, "onPressKey: code=" + Keyboard.printableCode(code)
                    + " single=" + isSinglePointer + " autoCaps=" + autoCaps + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onPressKey(code, this);
-        }
         if (code == Keyboard.CODE_SHIFT) {
             onPressShift();
         } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
@@ -341,9 +336,6 @@
             Log.d(TAG, "onReleaseKey: code=" + Keyboard.printableCode(code)
                     + " sliding=" + withSliding + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onReleaseKey(this, code, withSliding);
-        }
         if (code == Keyboard.CODE_SHIFT) {
             onReleaseShift(withSliding);
         } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
@@ -375,9 +367,6 @@
         if (DEBUG_EVENT) {
             Log.d(TAG, "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onLongPressTimeout(code, this);
-        }
         if (mIsAlphabetMode && code == Keyboard.CODE_SHIFT) {
             mLongPressShiftLockFired = true;
             mSwitchActions.hapticAndAudioFeedback(code);
@@ -509,9 +498,6 @@
         if (DEBUG_EVENT) {
             Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onCancelInput(isSinglePointer, this);
-        }
         // Switch back to the previous keyboard mode if the user cancels sliding input.
         if (isSinglePointer) {
             if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
@@ -543,9 +529,6 @@
                     + " single=" + isSinglePointer
                     + " autoCaps=" + autoCaps + " " + this);
         }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.keyboardState_onCodeInput(code, isSinglePointer, autoCaps, this);
-        }
 
         switch (mSwitchState) {
         case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 8c218c6..3b7c6ad 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);
@@ -131,71 +130,71 @@
         /* 23 */ "more_keys_for_nordic_row2_10",
         /* 24 */ "more_keys_for_nordic_row2_11",
         /* 25 */ "keylabel_for_east_slavic_row1_9",
-        /* 26 */ "keylabel_for_east_slavic_row2_1",
-        /* 27 */ "keylabel_for_east_slavic_row3_5",
-        /* 28 */ "more_keys_for_cyrillic_u",
-        /* 29 */ "more_keys_for_cyrillic_ye",
-        /* 30 */ "more_keys_for_cyrillic_en",
-        /* 31 */ "more_keys_for_cyrillic_ha",
-        /* 32 */ "more_keys_for_east_slavic_row2_1",
-        /* 33 */ "more_keys_for_cyrillic_o",
-        /* 34 */ "more_keys_for_cyrillic_soft_sign",
-        /* 35 */ "keylabel_for_south_slavic_row1_6",
-        /* 36 */ "keylabel_for_south_slavic_row2_11",
-        /* 37 */ "keylabel_for_south_slavic_row3_1",
-        /* 38 */ "keylabel_for_south_slavic_row3_8",
-        /* 39 */ "more_keys_for_cyrillic_ie",
-        /* 40 */ "more_keys_for_cyrillic_i",
-        /* 41 */ "more_keys_for_single_quote",
-        /* 42 */ "more_keys_for_double_quote",
-        /* 43 */ "more_keys_for_tablet_double_quote",
-        /* 44 */ "more_keys_for_currency_dollar",
-        /* 45 */ "more_keys_for_currency_euro",
-        /* 46 */ "more_keys_for_currency_pound",
-        /* 47 */ "more_keys_for_currency_general",
-        /* 48 */ "more_keys_for_punctuation",
-        /* 49 */ "more_keys_for_star",
-        /* 50 */ "more_keys_for_bullet",
-        /* 51 */ "more_keys_for_plus",
-        /* 52 */ "more_keys_for_left_parenthesis",
-        /* 53 */ "more_keys_for_right_parenthesis",
-        /* 54 */ "more_keys_for_less_than",
-        /* 55 */ "more_keys_for_greater_than",
-        /* 56 */ "more_keys_for_arabic_diacritics",
-        /* 57 */ "keyhintlabel_for_arabic_diacritics",
-        /* 58 */ "keylabel_for_symbols_1",
-        /* 59 */ "keylabel_for_symbols_2",
-        /* 60 */ "keylabel_for_symbols_3",
-        /* 61 */ "keylabel_for_symbols_4",
-        /* 62 */ "keylabel_for_symbols_5",
-        /* 63 */ "keylabel_for_symbols_6",
-        /* 64 */ "keylabel_for_symbols_7",
-        /* 65 */ "keylabel_for_symbols_8",
-        /* 66 */ "keylabel_for_symbols_9",
-        /* 67 */ "keylabel_for_symbols_0",
-        /* 68 */ "additional_more_keys_for_symbols_1",
-        /* 69 */ "additional_more_keys_for_symbols_2",
-        /* 70 */ "additional_more_keys_for_symbols_3",
-        /* 71 */ "additional_more_keys_for_symbols_4",
-        /* 72 */ "additional_more_keys_for_symbols_5",
-        /* 73 */ "additional_more_keys_for_symbols_6",
-        /* 74 */ "additional_more_keys_for_symbols_7",
-        /* 75 */ "additional_more_keys_for_symbols_8",
-        /* 76 */ "additional_more_keys_for_symbols_9",
-        /* 77 */ "additional_more_keys_for_symbols_0",
-        /* 78 */ "more_keys_for_symbols_1",
-        /* 79 */ "more_keys_for_symbols_2",
-        /* 80 */ "more_keys_for_symbols_3",
-        /* 81 */ "more_keys_for_symbols_4",
-        /* 82 */ "more_keys_for_symbols_5",
-        /* 83 */ "more_keys_for_symbols_6",
-        /* 84 */ "more_keys_for_symbols_7",
-        /* 85 */ "more_keys_for_symbols_8",
-        /* 86 */ "more_keys_for_symbols_9",
-        /* 87 */ "more_keys_for_symbols_0",
-        /* 88 */ "keylabel_for_comma",
-        /* 89 */ "more_keys_for_comma",
-        /* 90 */ "keylabel_for_symbols_exclamation",
+        /* 26 */ "keylabel_for_east_slavic_row1_12",
+        /* 27 */ "keylabel_for_east_slavic_row2_1",
+        /* 28 */ "keylabel_for_east_slavic_row2_11",
+        /* 29 */ "keylabel_for_east_slavic_row3_5",
+        /* 30 */ "more_keys_for_cyrillic_u",
+        /* 31 */ "more_keys_for_cyrillic_en",
+        /* 32 */ "more_keys_for_cyrillic_ghe",
+        /* 33 */ "more_keys_for_east_slavic_row2_1",
+        /* 34 */ "more_keys_for_cyrillic_o",
+        /* 35 */ "more_keys_for_cyrillic_soft_sign",
+        /* 36 */ "keylabel_for_south_slavic_row1_6",
+        /* 37 */ "keylabel_for_south_slavic_row2_11",
+        /* 38 */ "keylabel_for_south_slavic_row3_1",
+        /* 39 */ "keylabel_for_south_slavic_row3_8",
+        /* 40 */ "more_keys_for_cyrillic_ie",
+        /* 41 */ "more_keys_for_cyrillic_i",
+        /* 42 */ "more_keys_for_single_quote",
+        /* 43 */ "more_keys_for_double_quote",
+        /* 44 */ "more_keys_for_tablet_double_quote",
+        /* 45 */ "more_keys_for_currency_dollar",
+        /* 46 */ "more_keys_for_currency_euro",
+        /* 47 */ "more_keys_for_currency_pound",
+        /* 48 */ "more_keys_for_currency_general",
+        /* 49 */ "more_keys_for_punctuation",
+        /* 50 */ "more_keys_for_star",
+        /* 51 */ "more_keys_for_bullet",
+        /* 52 */ "more_keys_for_plus",
+        /* 53 */ "more_keys_for_left_parenthesis",
+        /* 54 */ "more_keys_for_right_parenthesis",
+        /* 55 */ "more_keys_for_less_than",
+        /* 56 */ "more_keys_for_greater_than",
+        /* 57 */ "more_keys_for_arabic_diacritics",
+        /* 58 */ "keyhintlabel_for_arabic_diacritics",
+        /* 59 */ "keylabel_for_symbols_1",
+        /* 60 */ "keylabel_for_symbols_2",
+        /* 61 */ "keylabel_for_symbols_3",
+        /* 62 */ "keylabel_for_symbols_4",
+        /* 63 */ "keylabel_for_symbols_5",
+        /* 64 */ "keylabel_for_symbols_6",
+        /* 65 */ "keylabel_for_symbols_7",
+        /* 66 */ "keylabel_for_symbols_8",
+        /* 67 */ "keylabel_for_symbols_9",
+        /* 68 */ "keylabel_for_symbols_0",
+        /* 69 */ "additional_more_keys_for_symbols_1",
+        /* 70 */ "additional_more_keys_for_symbols_2",
+        /* 71 */ "additional_more_keys_for_symbols_3",
+        /* 72 */ "additional_more_keys_for_symbols_4",
+        /* 73 */ "additional_more_keys_for_symbols_5",
+        /* 74 */ "additional_more_keys_for_symbols_6",
+        /* 75 */ "additional_more_keys_for_symbols_7",
+        /* 76 */ "additional_more_keys_for_symbols_8",
+        /* 77 */ "additional_more_keys_for_symbols_9",
+        /* 78 */ "additional_more_keys_for_symbols_0",
+        /* 79 */ "more_keys_for_symbols_1",
+        /* 80 */ "more_keys_for_symbols_2",
+        /* 81 */ "more_keys_for_symbols_3",
+        /* 82 */ "more_keys_for_symbols_4",
+        /* 83 */ "more_keys_for_symbols_5",
+        /* 84 */ "more_keys_for_symbols_6",
+        /* 85 */ "more_keys_for_symbols_7",
+        /* 86 */ "more_keys_for_symbols_8",
+        /* 87 */ "more_keys_for_symbols_9",
+        /* 88 */ "more_keys_for_symbols_0",
+        /* 89 */ "keylabel_for_comma",
+        /* 90 */ "more_keys_for_comma",
         /* 91 */ "keylabel_for_symbols_question",
         /* 92 */ "keylabel_for_symbols_semicolon",
         /* 93 */ "keylabel_for_symbols_percent",
@@ -211,22 +210,29 @@
         /* 103 */ "keylabel_for_apostrophe",
         /* 104 */ "keyhintlabel_for_apostrophe",
         /* 105 */ "more_keys_for_apostrophe",
-        /* 106 */ "more_keys_for_am_pm",
-        /* 107 */ "settings_as_more_key",
-        /* 108 */ "shortcut_as_more_key",
-        /* 109 */ "action_next_as_more_key",
-        /* 110 */ "action_previous_as_more_key",
-        /* 111 */ "label_to_more_symbol_key",
-        /* 112 */ "label_to_more_symbol_for_tablet_key",
-        /* 113 */ "label_tab_key",
-        /* 114 */ "label_to_phone_numeric_key",
-        /* 115 */ "label_to_phone_symbols_key",
-        /* 116 */ "label_time_am",
-        /* 117 */ "label_time_pm",
-        /* 118 */ "label_to_symbol_key_pcqwerty",
-        /* 119 */ "keylabel_for_popular_domain",
-        /* 120 */ "more_keys_for_popular_domain",
-        /* 121 */ "more_keys_for_smiley",
+        /* 106 */ "more_keys_for_q",
+        /* 107 */ "more_keys_for_x",
+        /* 108 */ "keylabel_for_q",
+        /* 109 */ "keylabel_for_w",
+        /* 110 */ "keylabel_for_y",
+        /* 111 */ "keylabel_for_x",
+        /* 112 */ "keylabel_for_spanish_row2_10",
+        /* 113 */ "more_keys_for_am_pm",
+        /* 114 */ "settings_as_more_key",
+        /* 115 */ "shortcut_as_more_key",
+        /* 116 */ "action_next_as_more_key",
+        /* 117 */ "action_previous_as_more_key",
+        /* 118 */ "label_to_more_symbol_key",
+        /* 119 */ "label_to_more_symbol_for_tablet_key",
+        /* 120 */ "label_tab_key",
+        /* 121 */ "label_to_phone_numeric_key",
+        /* 122 */ "label_to_phone_symbols_key",
+        /* 123 */ "label_time_am",
+        /* 124 */ "label_time_pm",
+        /* 125 */ "label_to_symbol_key_pcqwerty",
+        /* 126 */ "keylabel_for_popular_domain",
+        /* 127 */ "more_keys_for_popular_domain",
+        /* 128 */ "more_keys_for_smiley",
     };
 
     private static final String EMPTY = "";
@@ -237,41 +243,41 @@
         EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
         EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
         EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
-        EMPTY, EMPTY,
-        /* ~40 */
-        /* 41 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B",
+        EMPTY, EMPTY, EMPTY,
+        /* ~41 */
+        /* 42 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B",
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
         // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;</string>
-        /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB",
+        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB",
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
         // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
-        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+        /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
         // U+00A2: "¢" CENT SIGN
         // U+00A3: "£" POUND SIGN
         // U+20AC: "€" EURO SIGN
         // U+00A5: "¥" YEN SIGN
         // U+20B1: "₱" PESO SIGN
-        /* 44 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
-        /* 45 */ "\u00A2,\u00A3,$,\u00A5,\u20B1",
-        /* 46 */ "\u00A2,$,\u20AC,\u00A5,\u20B1",
-        /* 47 */ "\u00A2,$,\u20AC,\u00A3,\u00A5,\u20B1",
-        /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
+        /* 45 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
+        /* 46 */ "\u00A2,\u00A3,$,\u00A5,\u20B1",
+        /* 47 */ "\u00A2,$,\u20AC,\u00A5,\u20B1",
+        /* 48 */ "\u00A2,$,\u20AC,\u00A3,\u00A5,\u20B1",
+        /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
         // U+2020: "†" DAGGER
         // U+2021: "‡" DOUBLE DAGGER
         // U+2605: "★" BLACK STAR
-        /* 49 */ "\u2020,\u2021,\u2605",
+        /* 50 */ "\u2020,\u2021,\u2605",
         // U+266A: "♪" EIGHTH NOTE
         // U+2665: "♥" BLACK HEART SUIT
         // U+2660: "♠" BLACK SPADE SUIT
         // U+2666: "♦" BLACK DIAMOND SUIT
         // U+2663: "♣" BLACK CLUB SUIT
-        /* 50 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
+        /* 51 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
         // U+00B1: "±" PLUS-MINUS SIGN
-        /* 51 */ "\u00B1",
+        /* 52 */ "\u00B1",
         // The all letters need to be mirrored are found at
         // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
-        /* 52 */ "!fixedColumnOrder!3,<,{,[",
-        /* 53 */ "!fixedColumnOrder!3,>,},]",
+        /* 53 */ "!fixedColumnOrder!3,<,{,[",
+        /* 54 */ "!fixedColumnOrder!3,>,},]",
         // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK
         // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
         // U+2264: "≤" LESS-THAN OR EQUAL TO
@@ -287,51 +293,50 @@
         // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
         // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
         // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
-        /* 54 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB",
-        /* 55 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB",
-        /* 56 */ EMPTY,
+        /* 55 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB",
+        /* 56 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB",
         /* 57 */ EMPTY,
-        /* 58 */ "1",
-        /* 59 */ "2",
-        /* 60 */ "3",
-        /* 61 */ "4",
-        /* 62 */ "5",
-        /* 63 */ "6",
-        /* 64 */ "7",
-        /* 65 */ "8",
-        /* 66 */ "9",
-        /* 67 */ "0",
-        /* 68~ */
+        /* 58 */ EMPTY,
+        /* 59 */ "1",
+        /* 60 */ "2",
+        /* 61 */ "3",
+        /* 62 */ "4",
+        /* 63 */ "5",
+        /* 64 */ "6",
+        /* 65 */ "7",
+        /* 66 */ "8",
+        /* 67 */ "9",
+        /* 68 */ "0",
+        /* 69~ */
         EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
-        /* ~77 */
+        /* ~78 */
         // U+00B9: "¹" SUPERSCRIPT ONE
         // U+00BD: "½" VULGAR FRACTION ONE HALF
         // U+2153: "⅓" VULGAR FRACTION ONE THIRD
         // U+00BC: "¼" VULGAR FRACTION ONE QUARTER
         // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH
-        /* 78 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B",
+        /* 79 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B",
         // U+00B2: "²" SUPERSCRIPT TWO
         // U+2154: "⅔" VULGAR FRACTION TWO THIRDS
-        /* 79 */ "\u00B2,\u2154",
+        /* 80 */ "\u00B2,\u2154",
         // U+00B3: "³" SUPERSCRIPT THREE
         // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS
         // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS
-        /* 80 */ "\u00B3,\u00BE,\u215C",
+        /* 81 */ "\u00B3,\u00BE,\u215C",
         // U+2074: "⁴" SUPERSCRIPT FOUR
-        /* 81 */ "\u2074",
+        /* 82 */ "\u2074",
         // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS
-        /* 82 */ "\u215D",
-        /* 83 */ EMPTY,
+        /* 83 */ "\u215D",
+        /* 84 */ EMPTY,
         // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS
-        /* 84 */ "\u215E",
-        /* 85 */ EMPTY,
+        /* 85 */ "\u215E",
         /* 86 */ EMPTY,
+        /* 87 */ EMPTY,
         // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N
         // U+2205: "∅" EMPTY SET
-        /* 87 */ "\u207F,\u2205",
-        /* 88 */ ",",
-        /* 89 */ EMPTY,
-        /* 90 */ "!",
+        /* 88 */ "\u207F,\u2205",
+        /* 89 */ ",",
+        /* 90 */ EMPTY,
         /* 91 */ "?",
         /* 92 */ ";",
         /* 93 */ "%",
@@ -350,33 +355,94 @@
         /* 103 */ "\'",
         /* 104 */ "\"",
         /* 105 */ "\"",
-        /* 106 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm",
-        /* 107 */ "!icon/settings_key|!code/key_settings",
-        /* 108 */ "!icon/shortcut_key|!code/key_shortcut",
-        /* 109 */ "!hasLabels!,!text/label_next_key|!code/key_action_next",
-        /* 110 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous",
+        /* 106 */ EMPTY,
+        /* 107 */ EMPTY,
+        /* 108 */ "q",
+        /* 109 */ "w",
+        /* 110 */ "y",
+        /* 111 */ "x",
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        /* 112 */ "\u00F1",
+        /* 113 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm",
+        /* 114 */ "!icon/settings_key|!code/key_settings",
+        /* 115 */ "!icon/shortcut_key|!code/key_shortcut",
+        /* 116 */ "!hasLabels!,!text/label_next_key|!code/key_action_next",
+        /* 117 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous",
         // Label for "switch to more symbol" modifier key.  Must be short to fit on key!
-        /* 111 */ "= \\ <",
+        /* 118 */ "= \\ <",
         // Label for "switch to more symbol" modifier key on tablets.  Must be short to fit on key!
-        /* 112 */ "~ \\ {",
+        /* 119 */ "~ \\ {",
         // Label for "Tab" key.  Must be short to fit on key!
-        /* 113 */ "Tab",
+        /* 120 */ "Tab",
         // Label for "switch to phone numeric" key.  Must be short to fit on key!
-        /* 114 */ "123",
+        /* 121 */ "123",
         // Label for "switch to phone symbols" key.  Must be short to fit on key!
         // U+FF0A: "*" FULLWIDTH ASTERISK
         // U+FF03: "#" FULLWIDTH NUMBER SIGN
-        /* 115 */ "\uFF0A\uFF03",
+        /* 122 */ "\uFF0A\uFF03",
         // Key label for "ante meridiem"
-        /* 116 */ "AM",
+        /* 123 */ "AM",
         // Key label for "post meridiem"
-        /* 117 */ "PM",
+        /* 124 */ "PM",
         // Label for "switch to symbols" key on PC QWERTY layout
-        /* 118 */ "Sym",
-        /* 119 */ ".com",
+        /* 125 */ "Sym",
+        /* 126 */ ".com",
         // popular web domains for the locale - most popular, displayed on the keyboard
-        /* 120 */ "!hasLabels!,.net,.org,.gov,.edu",
-        /* 121 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
+        /* 127 */ "!hasLabels!,.net,.org,.gov,.edu",
+        /* 128 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
+    };
+
+    /* Language af: Afrikaans */
+    private static final String[] LANGUAGE_af = {
+        // This is the same as Dutch except more keys of y and demoting vowels with diaeresis.
+        // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+        // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+        // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+        // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+        // U+00E6: "æ" LATIN SMALL LETTER AE
+        // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+        // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+        // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+        /* 0 */ "\u00E1,\u00E2,\u00E4,\u00E0,\u00E6,\u00E3,\u00E5,\u0101",
+        // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+        // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+        // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+        // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+        // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+        // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+        // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+        /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+        // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+        // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+        // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+        // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+        // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+        // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+        // U+0133: "ij" LATIN SMALL LIGATURE IJ
+        /* 2 */ "\u00ED,\u00EC,\u00EF,\u00EE,\u012F,\u012B,\u0133",
+        // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+        // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+        // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+        // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+        // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+        // U+0153: "œ" LATIN SMALL LIGATURE OE
+        // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+        // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+        /* 3 */ "\u00F3,\u00F4,\u00F6,\u00F2,\u00F5,\u0153,\u00F8,\u014D",
+        // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+        // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+        // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+        // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+        // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+        /* 4 */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B",
+        /* 5 */ null,
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+        /* 6 */ "\u00F1,\u0144",
+        /* 7 */ null,
+        // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+        // U+0133: "ij" LATIN SMALL LIGATURE IJ
+        /* 8 */ "\u00FD,\u0133",
     };
 
     /* Language ar: Arabic */
@@ -384,33 +450,33 @@
         /* 0~ */
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null,
-        /* ~41 */
+        null, null, null, null, null, null, null, null, null, null, null, null, null,
+        /* ~42 */
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
-        /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
+        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
-        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
-        /* 44~ */
+        /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+        /* 45~ */
         null, null, null, null,
-        /* ~47 */
+        /* ~48 */
         // U+061F: "؟" ARABIC QUESTION MARK
         // U+060C: "،" ARABIC COMMA
         // U+061B: "؛" ARABIC SEMICOLON
-        /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+        /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
         // U+2605: "★" BLACK STAR
         // U+066D: "٭" ARABIC FIVE POINTED STAR
-        /* 49 */ "\u2605,\u066D",
+        /* 50 */ "\u2605,\u066D",
         // U+266A: "♪" EIGHTH NOTE
-        /* 50 */ "\u266A",
-        /* 51 */ null,
+        /* 51 */ "\u266A",
+        /* 52 */ null,
         // The all letters need to be mirrored are found at
         // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
         // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS
         // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS
-        /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
-        /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+        /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
+        /* 54 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
         // U+2264: "≤" LESS-THAN OR EQUAL TO
         // U+2265: "≥" GREATER-THAN EQUAL TO
         // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
@@ -426,8 +492,8 @@
         // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
         // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
         // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
-        /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
-        /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+        /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
+        /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
         // U+0655: "ٕ" ARABIC HAMZA BELOW
         // U+0654: "ٔ" ARABIC HAMZA ABOVE
         // U+0652: "ْ" ARABIC SUKUN
@@ -443,47 +509,46 @@
         // U+064E: "َ" ARABIC FATHA
         // U+0640: "ـ" ARABIC TATWEEL
         // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label.
-        /* 56 */ "!fixedColumnOrder!7,\u0655,\u0654,\u0652,\u064D,\u064C,\u064B,\u0651,\u0656,\u0670,\u0653,\u0650,\u064F,\u064E,\u0640\u0640\u0640|\u0640",
-        /* 57 */ "\u0651",
+        /* 57 */ "!fixedColumnOrder!7,\u0655,\u0654,\u0652,\u064D,\u064C,\u064B,\u0651,\u0656,\u0670,\u0653,\u0650,\u064F,\u064E,\u0640\u0640\u0640|\u0640",
+        /* 58 */ "\u0651",
         // U+0661: "١" ARABIC-INDIC DIGIT ONE
-        /* 58 */ "\u0661",
+        /* 59 */ "\u0661",
         // U+0662: "٢" ARABIC-INDIC DIGIT TWO
-        /* 59 */ "\u0662",
+        /* 60 */ "\u0662",
         // U+0663: "٣" ARABIC-INDIC DIGIT THREE
-        /* 60 */ "\u0663",
+        /* 61 */ "\u0663",
         // U+0664: "٤" ARABIC-INDIC DIGIT FOUR
-        /* 61 */ "\u0664",
+        /* 62 */ "\u0664",
         // U+0665: "٥" ARABIC-INDIC DIGIT FIVE
-        /* 62 */ "\u0665",
+        /* 63 */ "\u0665",
         // U+0666: "٦" ARABIC-INDIC DIGIT SIX
-        /* 63 */ "\u0666",
+        /* 64 */ "\u0666",
         // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN
-        /* 64 */ "\u0667",
+        /* 65 */ "\u0667",
         // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT
-        /* 65 */ "\u0668",
+        /* 66 */ "\u0668",
         // U+0669: "٩" ARABIC-INDIC DIGIT NINE
-        /* 66 */ "\u0669",
+        /* 67 */ "\u0669",
         // U+0660: "٠" ARABIC-INDIC DIGIT ZERO
-        /* 67 */ "\u0660",
-        /* 68 */ "1",
-        /* 69 */ "2",
-        /* 70 */ "3",
-        /* 71 */ "4",
-        /* 72 */ "5",
-        /* 73 */ "6",
-        /* 74 */ "7",
-        /* 75 */ "8",
-        /* 76 */ "9",
+        /* 68 */ "\u0660",
+        /* 69 */ "1",
+        /* 70 */ "2",
+        /* 71 */ "3",
+        /* 72 */ "4",
+        /* 73 */ "5",
+        /* 74 */ "6",
+        /* 75 */ "7",
+        /* 76 */ "8",
+        /* 77 */ "9",
         // U+066B: "٫" ARABIC DECIMAL SEPARATOR
         // U+066C: "٬" ARABIC THOUSANDS SEPARATOR
-        /* 77 */ "0,\u066B,\u066C",
-        /* 78~ */
+        /* 78 */ "0,\u066B,\u066C",
+        /* 79~ */
         null, null, null, null, null, null, null, null, null, null,
-        /* ~87 */
+        /* ~88 */
         // U+060C: "،" ARABIC COMMA
-        /* 88 */ "\u060C",
-        /* 89 */ "\\,",
-        /* 90 */ null,
+        /* 89 */ "\u060C",
+        /* 90 */ "\\,",
         /* 91 */ "\u061F",
         /* 92 */ "\u061B",
         // U+066A: "٪" ARABIC PERCENT SIGN
@@ -512,19 +577,24 @@
         /* ~24 */
         // U+045E: "ў" CYRILLIC SMALL LETTER SHORT U
         /* 25 */ "\u045E",
+        // U+0451: "ё" CYRILLIC SMALL LETTER IO
+        /* 26 */ "\u0451",
         // U+044B: "ы" CYRILLIC SMALL LETTER YERU
-        /* 26 */ "\u044B",
+        /* 27 */ "\u044B",
+        // U+044D: "э" CYRILLIC SMALL LETTER E
+        /* 28 */ "\u044D",
         // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
-        /* 27 */ "\u0456",
-        /* 28~ */
-        null, null, null,
-        /* ~30 */
+        /* 29 */ "\u0456",
+        /* 30~ */
+        null, null, null, null, null,
+        /* ~34 */
         // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 31 */ "\u044A",
-        /* 32 */ null,
-        /* 33 */ null,
-        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 34 */ "\u044A",
+        /* 35 */ "\u044A",
+        /* 36~ */
+        null, null, null, null,
+        /* ~39 */
+        // U+0451: "ё" CYRILLIC SMALL LETTER IO
+        /* 40 */ "\u0451",
     };
 
     /* Language ca: Catalan */
@@ -802,6 +872,144 @@
         /* 7 */ "\u00E7",
     };
 
+    /* Language eo: Esperanto */
+    private static final String[] LANGUAGE_eo = {
+        // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+        // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+        // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+        // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+        // U+00E6: "æ" LATIN SMALL LETTER AE
+        // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+        // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+        // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+        // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE
+        // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+        // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+        /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA",
+        // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+        // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+        // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+        // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+        // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+        // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+        // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+        // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+        /* 1 */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+        // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+        // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+        // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+        // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE
+        // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+        // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+        // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+        // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+        // U+0133: "ij" LATIN SMALL LIGATURE IJ
+        /* 2 */ "\u00ED,\u00EE,\u00EF,\u0129,\u00EC,\u012F,\u012B,\u0131,\u0133",
+        // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+        // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+        // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+        // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+        // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+        // U+0153: "œ" LATIN SMALL LIGATURE OE
+        // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+        // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+        // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+        // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+        /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D,\u0151,\u00BA",
+        // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+        // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+        // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+        // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+        // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+        // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+        // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE
+        // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+        // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+        // U+00B5: "µ" MICRO SIGN
+        /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5",
+        // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+        // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+        // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+        // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW
+        // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+        /* 5 */ "\u00DF,\u0161,\u015B,\u0219,\u015F",
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+        // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+        // U+0148: "ň" LATIN SMALL LETTER N WITH CARON
+        // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
+        // U+014B: "ŋ" LATIN SMALL LETTER ENG
+        /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B",
+        // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+        // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+        // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+        // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE
+        /* 7 */ "\u0107,\u010D,\u00E7,\u010B",
+        // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+        // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX
+        // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+        // U+00FE: "þ" LATIN SMALL LETTER THORN
+        /* 8 */ "y,\u00FD,\u0177,\u00FF,\u00FE",
+        // U+00F0: "ð" LATIN SMALL LETTER ETH
+        // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+        // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE
+        /* 9 */ "\u00F0,\u010F,\u0111",
+        // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+        // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+        // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA
+        /* 10 */ "\u0159,\u0155,\u0157",
+        // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+        // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW
+        // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+        // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE
+        /* 11 */ "\u0165,\u021B,\u0163,\u0167",
+        // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+        // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+        // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+        /* 12 */ "\u017A,\u017C,\u017E",
+        // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+        // U+0138: "ĸ" LATIN SMALL LETTER KRA
+        /* 13 */ "\u0137,\u0138",
+        // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+        // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+        // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+        // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT
+        // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+        /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142",
+        // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+        // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE
+        // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA
+        /* 15 */ "\u011F,\u0121,\u0123",
+        // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX
+        /* 16 */ "w,\u0175",
+        // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX
+        // U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE
+        /* 17 */ "\u0125,\u0127",
+        /* 18 */ null,
+        // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX
+        /* 19 */ "w,\u0175",
+        /* 20~ */
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null,
+        /* ~105 */
+        /* 106 */ "q",
+        /* 107 */ "x",
+        // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX
+        /* 108 */ "\u015D",
+        // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX
+        /* 109 */ "\u011D",
+        // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE
+        /* 110 */ "\u016D",
+        // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX
+        /* 111 */ "\u0109",
+        // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX
+        /* 112 */ "\u0135",
+    };
+
     /* Language es: Spanish */
     private static final String[] LANGUAGE_es = {
         // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
@@ -857,31 +1065,22 @@
         /* 8~ */
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null,
-        /* ~47 */
-        // U+00A1: "¡" INVERTED EXCLAMATION MARK
-        // U+00BF: "¿" INVERTED QUESTION MARK
-        /* 48 */ "!fixedColumnOrder!9,\",\',#,-,\u00A1,!,\u00BF,\\,,?,@,&,\\%,+,;,:,/,(,)",
-        /* 49~ */
-        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null,
-        /* ~89 */
+        /* ~48 */
         // U+00A1: "¡" INVERTED EXCLAMATION MARK
-        /* 90 */ "\u00A1",
         // U+00BF: "¿" INVERTED QUESTION MARK
-        /* 91 */ "\u00BF",
-        /* 92 */ null,
-        /* 93 */ null,
-        /* 94 */ "!",
-        /* 95 */ "?",
-        /* 96~ */
-        null, null, null,
-        /* ~98 */
-        /* 99 */ "\u00A1",
-        /* 100 */ "\u00A1,!",
-        /* 101 */ "\u00BF",
-        /* 102 */ "\u00BF,?",
+        /* 49 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)",
+        /* 50~ */
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null,
+        /* ~99 */
+        // U+00A1: "¡" INVERTED EXCLAMATION MARK
+        /* 100 */ "!,\u00A1",
+        /* 101 */ null,
+        // U+00BF: "¿" INVERTED QUESTION MARK
+        /* 102 */ "?,\u00BF",
     };
 
     /* Language et: Estonian */
@@ -989,33 +1188,33 @@
         /* 0~ */
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null,
-        /* ~41 */
+        null, null, null, null, null, null, null, null, null, null, null, null, null,
+        /* ~42 */
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
-        /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'",
+        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'",
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
-        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
-        /* 44~ */
+        /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+        /* 45~ */
         null, null, null, null,
-        /* ~47 */
+        /* ~48 */
         // U+061F: "؟" ARABIC QUESTION MARK
         // U+060C: "،" ARABIC COMMA
         // U+061B: "؛" ARABIC SEMICOLON
-        /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+        /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
         // U+2605: "★" BLACK STAR
         // U+066D: "٭" ARABIC FIVE POINTED STAR
-        /* 49 */ "\u2605,\u066D",
+        /* 50 */ "\u2605,\u066D",
         // U+266A: "♪" EIGHTH NOTE
-        /* 50 */ "\u266A",
-        /* 51 */ null,
+        /* 51 */ "\u266A",
+        /* 52 */ null,
         // The all letters need to be mirrored are found at
         // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
         // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS
         // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS
-        /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
-        /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+        /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
+        /* 54 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
         // U+2264: "≤" LESS-THAN OR EQUAL TO
         // U+2265: "≥" GREATER-THAN EQUAL TO
         // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
@@ -1031,8 +1230,8 @@
         // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
         // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
         // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
-        /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>",
-        /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<",
+        /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>",
+        /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<",
         // U+0655: "ٕ" ARABIC HAMZA BELOW
         // U+0652: "ْ" ARABIC SUKUN
         // U+0651: "ّ" ARABIC SHADDA
@@ -1048,47 +1247,46 @@
         // U+064E: "َ" ARABIC FATHA
         // U+0640: "ـ" ARABIC TATWEEL
         // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label.
-        /* 56 */ "!fixedColumnOrder!7,\u0655,\u0652,\u0651,\u064C,\u064D,\u064B,\u0654,\u0656,\u0670,\u0653,\u064F,\u0650,\u064E,\u0640\u0640\u0640|\u0640",
-        /* 57 */ "\u064B",
+        /* 57 */ "!fixedColumnOrder!7,\u0655,\u0652,\u0651,\u064C,\u064D,\u064B,\u0654,\u0656,\u0670,\u0653,\u064F,\u0650,\u064E,\u0640\u0640\u0640|\u0640",
+        /* 58 */ "\u064B",
         // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE
-        /* 58 */ "\u06F1",
+        /* 59 */ "\u06F1",
         // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO
-        /* 59 */ "\u06F2",
+        /* 60 */ "\u06F2",
         // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE
-        /* 60 */ "\u06F3",
+        /* 61 */ "\u06F3",
         // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR
-        /* 61 */ "\u06F4",
+        /* 62 */ "\u06F4",
         // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE
-        /* 62 */ "\u06F5",
+        /* 63 */ "\u06F5",
         // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX
-        /* 63 */ "\u06F6",
+        /* 64 */ "\u06F6",
         // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN
-        /* 64 */ "\u06F7",
+        /* 65 */ "\u06F7",
         // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT
-        /* 65 */ "\u06F8",
+        /* 66 */ "\u06F8",
         // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE
-        /* 66 */ "\u06F9",
+        /* 67 */ "\u06F9",
         // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO
-        /* 67 */ "\u06F0",
-        /* 68 */ "1",
-        /* 69 */ "2",
-        /* 70 */ "3",
-        /* 71 */ "4",
-        /* 72 */ "5",
-        /* 73 */ "6",
-        /* 74 */ "7",
-        /* 75 */ "8",
-        /* 76 */ "9",
+        /* 68 */ "\u06F0",
+        /* 69 */ "1",
+        /* 70 */ "2",
+        /* 71 */ "3",
+        /* 72 */ "4",
+        /* 73 */ "5",
+        /* 74 */ "6",
+        /* 75 */ "7",
+        /* 76 */ "8",
+        /* 77 */ "9",
         // U+066B: "٫" ARABIC DECIMAL SEPARATOR
         // U+066C: "٬" ARABIC THOUSANDS SEPARATOR
-        /* 77 */ "0,\u066B,\u066C",
-        /* 78~ */
+        /* 78 */ "0,\u066B,\u066C",
+        /* 79~ */
         null, null, null, null, null, null, null, null, null, null,
-        /* ~87 */
+        /* ~88 */
         // U+060C: "،" ARABIC COMMA
-        /* 88 */ "\u060C",
-        /* 89 */ "\\,",
-        /* 90 */ null,
+        /* 89 */ "\u060C",
+        /* 90 */ "\\,",
         /* 91 */ "\u061F",
         /* 92 */ "\u061B",
         // U+066A: "٪" ARABIC PERCENT SIGN
@@ -1219,38 +1417,38 @@
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null, null,
-        /* ~57 */
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        /* ~58 */
         // U+0967: "१" DEVANAGARI DIGIT ONE
-        /* 58 */ "\u0967",
+        /* 59 */ "\u0967",
         // U+0968: "२" DEVANAGARI DIGIT TWO
-        /* 59 */ "\u0968",
+        /* 60 */ "\u0968",
         // U+0969: "३" DEVANAGARI DIGIT THREE
-        /* 60 */ "\u0969",
+        /* 61 */ "\u0969",
         // U+096A: "४" DEVANAGARI DIGIT FOUR
-        /* 61 */ "\u096A",
+        /* 62 */ "\u096A",
         // U+096B: "५" DEVANAGARI DIGIT FIVE
-        /* 62 */ "\u096B",
+        /* 63 */ "\u096B",
         // U+096C: "६" DEVANAGARI DIGIT SIX
-        /* 63 */ "\u096C",
+        /* 64 */ "\u096C",
         // U+096D: "७" DEVANAGARI DIGIT SEVEN
-        /* 64 */ "\u096D",
+        /* 65 */ "\u096D",
         // U+096E: "८" DEVANAGARI DIGIT EIGHT
-        /* 65 */ "\u096E",
+        /* 66 */ "\u096E",
         // U+096F: "९" DEVANAGARI DIGIT NINE
-        /* 66 */ "\u096F",
+        /* 67 */ "\u096F",
         // U+0966: "०" DEVANAGARI DIGIT ZERO
-        /* 67 */ "\u0966",
-        /* 68 */ "1",
-        /* 69 */ "2",
-        /* 70 */ "3",
-        /* 71 */ "4",
-        /* 72 */ "5",
-        /* 73 */ "6",
-        /* 74 */ "7",
-        /* 75 */ "8",
-        /* 76 */ "9",
-        /* 77 */ "0",
+        /* 68 */ "\u0966",
+        /* 69 */ "1",
+        /* 70 */ "2",
+        /* 71 */ "3",
+        /* 72 */ "4",
+        /* 73 */ "5",
+        /* 74 */ "6",
+        /* 75 */ "7",
+        /* 76 */ "8",
+        /* 77 */ "9",
+        /* 78 */ "0",
     };
 
     /* Language hr: Croatian */
@@ -1438,27 +1636,27 @@
         /* 0~ */
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null, null, null, null, null, null, null, null,
-        /* ~41 */
+        null, null, null, null, null, null, null, null, null, null, null, null, null,
+        /* ~42 */
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
-        /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
+        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
-        /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
-        /* 44~ */
+        /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+        /* 45~ */
         null, null, null, null, null,
-        /* ~48 */
+        /* ~49 */
         // U+2605: "★" BLACK STAR
-        /* 49 */ "\u2605",
-        /* 50 */ null,
+        /* 50 */ "\u2605",
+        /* 51 */ null,
         // U+00B1: "±" PLUS-MINUS SIGN
         // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN
-        /* 51 */ "\u00B1,\uFB29",
+        /* 52 */ "\u00B1,\uFB29",
         // The all letters need to be mirrored are found at
         // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
-        /* 52 */ "!fixedColumnOrder!3,<|>,{|},[|]",
-        /* 53 */ "!fixedColumnOrder!3,>|<,}|{,]|[",
+        /* 53 */ "!fixedColumnOrder!3,<|>,{|},[|]",
+        /* 54 */ "!fixedColumnOrder!3,>|<,}|{,]|[",
         // U+2264: "≤" LESS-THAN OR EQUAL TO
         // U+2265: "≥" GREATER-THAN EQUAL TO
         // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
@@ -1474,8 +1672,8 @@
         // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
         // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
         // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
-        /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
-        /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+        /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
+        /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
     };
 
     /* Language ky: Kirghiz */
@@ -1486,22 +1684,29 @@
         /* ~24 */
         // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
         /* 25 */ "\u0449",
+        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+        /* 26 */ "\u044A",
         // U+044B: "ы" CYRILLIC SMALL LETTER YERU
-        /* 26 */ "\u044B",
+        /* 27 */ "\u044B",
+        // U+044D: "э" CYRILLIC SMALL LETTER E
+        /* 28 */ "\u044D",
         // U+0438: "и" CYRILLIC SMALL LETTER I
-        /* 27 */ "\u0438",
+        /* 29 */ "\u0438",
         // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U
-        /* 28 */ "\u04AF",
-        /* 29 */ null,
+        /* 30 */ "\u04AF",
         // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER
-        /* 30 */ "\u04A3",
-        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 31 */ "\u044A",
+        /* 31 */ "\u04A3",
         /* 32 */ null,
+        /* 33 */ null,
         // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O
-        /* 33 */ "\u04E9",
+        /* 34 */ "\u04E9",
         // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 34 */ "\u044A",
+        /* 35 */ "\u044A",
+        /* 36~ */
+        null, null, null, null,
+        /* ~39 */
+        // U+0451: "ё" CYRILLIC SMALL LETTER IO
+        /* 40 */ "\u0451",
     };
 
     /* Language lt: Lithuanian */
@@ -1688,21 +1893,21 @@
         /* 0~ */
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null,
-        /* ~34 */
+        null, null, null, null, null, null,
+        /* ~35 */
         // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
-        /* 35 */ "\u0455",
+        /* 36 */ "\u0455",
         // U+045C: "ќ" CYRILLIC SMALL LETTER KJE
-        /* 36 */ "\u045C",
+        /* 37 */ "\u045C",
         // U+0437: "з" CYRILLIC SMALL LETTER ZE
-        /* 37 */ "\u0437",
+        /* 38 */ "\u0437",
         // U+0453: "ѓ" CYRILLIC SMALL LETTER GJE
-        /* 38 */ "\u0453",
+        /* 39 */ "\u0453",
         // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
-        /* 39 */ "\u0450",
+        /* 40 */ "\u0450",
         // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
-        /* 40 */ "\u045D",
-        /* 41 */ null,
+        /* 41 */ "\u045D",
+        /* 42 */ null,
         // U+2018: "‘" LEFT SINGLE QUOTATION MARK
         // U+2019: "’" RIGHT SINGLE QUOTATION MARK
         // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
@@ -1713,10 +1918,10 @@
         // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
         // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201E;,&#x201C;,&#x201D;,&#x201F;,&#x00AB;,&#x00BB;</string>
-        /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
+        /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
         // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
-        /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+        /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
     };
 
     /* Language nb: Norwegian Bokmål */
@@ -1978,20 +2183,24 @@
         /* ~24 */
         // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
         /* 25 */ "\u0449",
+        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+        /* 26 */ "\u044A",
         // U+044B: "ы" CYRILLIC SMALL LETTER YERU
-        /* 26 */ "\u044B",
+        /* 27 */ "\u044B",
+        // U+044D: "э" CYRILLIC SMALL LETTER E
+        /* 28 */ "\u044D",
         // U+0438: "и" CYRILLIC SMALL LETTER I
-        /* 27 */ "\u0438",
-        /* 28 */ null,
+        /* 29 */ "\u0438",
+        /* 30~ */
+        null, null, null, null, null,
+        /* ~34 */
+        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+        /* 35 */ "\u044A",
+        /* 36~ */
+        null, null, null, null,
+        /* ~39 */
         // U+0451: "ё" CYRILLIC SMALL LETTER IO
-        /* 29 */ "\u0451",
-        /* 30 */ null,
-        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 31 */ "\u044A",
-        /* 32 */ null,
-        /* 33 */ null,
-        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 34 */ "\u044A",
+        /* 40 */ "\u0451",
     };
 
     /* Language sk: Slovak */
@@ -2109,21 +2318,40 @@
         /* 0~ */
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
         null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-        null, null, null, null, null,
-        /* ~34 */
+        null, null, null, null, null, null,
+        /* ~35 */
+        // TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified.
+        // BEGIN: More keys definitions for Serbian (Latin)
+        // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+        // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+        // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+        // <string name="more_keys_for_s">&#x0161;,&#x00DF;,&#x015B;</string>
+        // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+        // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+        // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+        // <string name="more_keys_for_c">&#x010D;,&#x00E7;,&#x0107;</string>
+        // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+        // <string name="more_keys_for_d">&#x010F;</string>
+        // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+        // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+        // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+        // <string name="more_keys_for_z">&#x017E;,&#x017A;,&#x017C;</string>
+        // END: More keys definitions for Serbian (Latin)
+        // BEGIN: More keys definitions for Serbian (Cyrillic)
         // U+0437: "з" CYRILLIC SMALL LETTER ZE
-        /* 35 */ "\u0437",
+        /* 36 */ "\u0437",
         // U+045B: "ћ" CYRILLIC SMALL LETTER TSHE
-        /* 36 */ "\u045B",
+        /* 37 */ "\u045B",
         // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
-        /* 37 */ "\u0455",
+        /* 38 */ "\u0455",
         // U+0452: "ђ" CYRILLIC SMALL LETTER DJE
-        /* 38 */ "\u0452",
+        /* 39 */ "\u0452",
         // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
-        /* 39 */ "\u0450",
+        /* 40 */ "\u0450",
         // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
-        /* 40 */ "\u045D",
-        /* 41 */ null,
+        /* 41 */ "\u045D",
+        /* 42 */ null,
+        // END: More keys definitions for Serbian (Cyrillic)
         // U+2018: "‘" LEFT SINGLE QUOTATION MARK
         // U+2019: "’" RIGHT SINGLE QUOTATION MARK
         // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
@@ -2134,10 +2362,10 @@
         // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
         // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201E;,&#x201C;,&#x201D;,&#x201F;,&#x00AB;,&#x00BB;</string>
-        /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
+        /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
         // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
         // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
-        /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+        /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
     };
 
     /* Language sv: Swedish */
@@ -2182,6 +2410,111 @@
         /* 24 */ "\u00E6",
     };
 
+    /* Language sw: Swahili */
+    private static final String[] LANGUAGE_sw = {
+        // This is the same as English except more_keys_for_g.
+        // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+        // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+        // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+        // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+        // U+00E6: "æ" LATIN SMALL LETTER AE
+        // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+        // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+        // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+        /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+        // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+        // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+        // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+        // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+        // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+        /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113",
+        // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+        // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+        // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+        // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+        // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+        /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC",
+        // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+        // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+        // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+        // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+        // U+0153: "œ" LATIN SMALL LIGATURE OE
+        // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+        // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+        // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+        /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5",
+        // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+        // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+        // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+        // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+        // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+        /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B",
+        // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+        /* 5 */ "\u00DF",
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        /* 6 */ "\u00F1",
+        // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+        /* 7 */ "\u00E7",
+        /* 8~ */
+        null, null, null, null, null, null, null,
+        /* ~14 */
+        /* 15 */ "g\'",
+    };
+
+    /* Language tl: Tagalog */
+    private static final String[] LANGUAGE_tl = {
+        // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+        // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+        // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+        // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+        // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+        // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+        // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+        // U+00E6: "æ" LATIN SMALL LETTER AE
+        // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+        // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+        /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA",
+        // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+        // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+        // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+        // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+        // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+        // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+        // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+        /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113",
+        // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+        // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+        // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+        // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+        // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+        // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+        /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B",
+        // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+        // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+        // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+        // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+        // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+        // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+        // U+0153: "œ" LATIN SMALL LIGATURE OE
+        // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+        // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+        /* 3 */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA",
+        // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+        // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+        // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+        // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+        // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+        /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B",
+        /* 5 */ null,
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+        /* 6 */ "\u00F1,\u0144",
+        // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+        // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+        // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+        /* 7 */ "\u00E7,\u0107,\u010D",
+    };
+
     /* Language tr: Turkish */
     private static final String[] LANGUAGE_tr = {
         // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
@@ -2235,20 +2568,23 @@
         /* ~24 */
         // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
         /* 25 */ "\u0449",
-        // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
-        /* 26 */ "\u0456",
-        // U+0438: "и" CYRILLIC SMALL LETTER I
-        /* 27 */ "\u0438",
-        /* 28~ */
-        null, null, null,
-        /* ~30 */
-        // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 31 */ "\u044A",
         // U+0457: "ї" CYRILLIC SMALL LETTER YI
-        /* 32 */ "\u0457",
-        /* 33 */ null,
+        /* 26 */ "\u0457",
+        // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
+        /* 27 */ "\u0456",
+        // U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE
+        /* 28 */ "\u0454",
+        // U+0438: "и" CYRILLIC SMALL LETTER I
+        /* 29 */ "\u0438",
+        /* 30 */ null,
+        /* 31 */ null,
+        // U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN
+        /* 32 */ "\u0491",
+        // U+0457: "ї" CYRILLIC SMALL LETTER YI
+        /* 33 */ "\u0457",
+        /* 34 */ null,
         // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
-        /* 34 */ "\u044A",
+        /* 35 */ "\u044A",
     };
 
     /* Language vi: Vietnamese */
@@ -2332,6 +2668,53 @@
         /* 9 */ "\u0111",
     };
 
+    /* Language zu: Zulu */
+    private static final String[] LANGUAGE_zu = {
+        // This is the same as English
+        // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+        // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+        // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+        // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+        // U+00E6: "æ" LATIN SMALL LETTER AE
+        // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+        // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+        // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+        /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+        // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+        // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+        // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+        // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+        // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+        /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113",
+        // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+        // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+        // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+        // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+        // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+        /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC",
+        // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+        // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+        // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+        // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+        // U+0153: "œ" LATIN SMALL LIGATURE OE
+        // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+        // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+        // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+        /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5",
+        // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+        // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+        // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+        // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+        // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+        /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B",
+        // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+        /* 5 */ "\u00DF",
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        /* 6 */ "\u00F1",
+        // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+        /* 7 */ "\u00E7",
+    };
+
     /* Language zz: No language */
     private static final String[] LANGUAGE_zz = {
         // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
@@ -2457,6 +2840,7 @@
 
     private static final Object[] LANGUAGES_AND_TEXTS = {
         "DEFAULT", LANGUAGE_DEFAULT, /* default */
+        "af", LANGUAGE_af, /* Afrikaans */
         "ar", LANGUAGE_ar, /* Arabic */
         "be", LANGUAGE_be, /* Belarusian */
         "ca", LANGUAGE_ca, /* Catalan */
@@ -2464,6 +2848,7 @@
         "da", LANGUAGE_da, /* Danish */
         "de", LANGUAGE_de, /* German */
         "en", LANGUAGE_en, /* English */
+        "eo", LANGUAGE_eo, /* Esperanto */
         "es", LANGUAGE_es, /* Spanish */
         "et", LANGUAGE_et, /* Estonian */
         "fa", LANGUAGE_fa, /* Persian */
@@ -2490,9 +2875,12 @@
         "sl", LANGUAGE_sl, /* Slovenian */
         "sr", LANGUAGE_sr, /* Serbian */
         "sv", LANGUAGE_sv, /* Swedish */
+        "sw", LANGUAGE_sw, /* Swahili */
+        "tl", LANGUAGE_tl, /* Tagalog */
         "tr", LANGUAGE_tr, /* Turkish */
         "uk", LANGUAGE_uk, /* Ukrainian */
         "vi", LANGUAGE_vi, /* Vietnamese */
+        "zu", LANGUAGE_zu, /* Zulu */
         "zz", LANGUAGE_zz, /* No language */
     };
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
new file mode 100644
index 0000000..f54617c
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
@@ -0,0 +1,40 @@
+/*
+ * 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.keyboard.Key;
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.HashMap;
+
+public class KeysCache {
+    private final HashMap<Key, Key> mMap = CollectionUtils.newHashMap();
+
+    public void clear() {
+        mMap.clear();
+    }
+
+    public Key get(final Key key) {
+        final Key existingKey = mMap.get(key);
+        if (existingKey != null) {
+            // Reuse the existing element that equals to "key" without adding "key" to the map.
+            return existingKey;
+        }
+        mMap.put(key, key);
+        return key;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
new file mode 100644
index 0000000..550391b
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -0,0 +1,86 @@
+/*
+ * 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.text.TextUtils;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.StringUtils;
+
+import java.util.Locale;
+
+public final class MoreKeySpec {
+    public final int mCode;
+    public final String mLabel;
+    public final String mOutputText;
+    public final int mIconId;
+
+    public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale,
+            final KeyboardCodesSet codesSet) {
+        mLabel = KeySpecParser.toUpperCaseOfStringForLocale(
+                KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale);
+        final int code = KeySpecParser.toUpperCaseOfCodeForLocale(
+                KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale);
+        if (code == Keyboard.CODE_UNSPECIFIED) {
+            // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
+            // upper case representation ("SS").
+            mCode = Keyboard.CODE_OUTPUT_TEXT;
+            mOutputText = mLabel;
+        } else {
+            mCode = code;
+            mOutputText = KeySpecParser.toUpperCaseOfStringForLocale(
+                    KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale);
+        }
+        mIconId = KeySpecParser.getIconId(moreKeySpec);
+    }
+
+    @Override
+    public int hashCode() {
+        int hashCode = 1;
+        hashCode = 31 + mCode;
+        hashCode = hashCode * 31 + mIconId;
+        hashCode = hashCode * 31 + (mLabel == null ? 0 : mLabel.hashCode());
+        hashCode = hashCode * 31 + (mOutputText == null ? 0 : mOutputText.hashCode());
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) return true;
+        if (o instanceof MoreKeySpec) {
+            final MoreKeySpec other = (MoreKeySpec)o;
+            return mCode == other.mCode
+                    && mIconId == other.mIconId
+                    && TextUtils.equals(mLabel, other.mLabel)
+                    && TextUtils.equals(mOutputText, other.mOutputText);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
+                : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
+        final String output = (mCode == Keyboard.CODE_OUTPUT_TEXT ? mOutputText
+                : Keyboard.printableCode(mCode));
+        if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
+            return output;
+        } else {
+            return label + "|" + output;
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index 5db65c6..c1a5cbe 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -18,72 +18,164 @@
 
 import android.util.Log;
 
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.latin.CollectionUtils;
 
-import java.util.Iterator;
-import java.util.LinkedList;
+import java.util.ArrayList;
 
 public class PointerTrackerQueue {
     private static final String TAG = PointerTrackerQueue.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    private final LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
-
-    public synchronized void add(PointerTracker tracker) {
-        mQueue.add(tracker);
+    public interface Element {
+        public boolean isModifier();
+        public boolean isInSlidingKeyInput();
+        public void onPhantomUpEvent(long eventTime);
     }
 
-    public synchronized void remove(PointerTracker tracker) {
-        mQueue.remove(tracker);
+    private static final int INITIAL_CAPACITY = 10;
+    private final ArrayList<Element> mExpandableArrayOfActivePointers =
+            CollectionUtils.newArrayList(INITIAL_CAPACITY);
+    private int mArraySize = 0;
+
+    public synchronized int size() {
+        return mArraySize;
     }
 
-    public synchronized void releaseAllPointersOlderThan(PointerTracker tracker,
-            long eventTime) {
+    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(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 Element getOldestElement() {
+        return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0);
+    }
+
+    public synchronized void releaseAllPointersOlderThan(final Element pointer,
+            final long eventTime) {
         if (DEBUG) {
-            Log.d(TAG, "releaseAllPoniterOlderThan: [" + tracker.mPointerId + "] " + this);
+            Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this);
         }
-        if (!mQueue.contains(tracker)) {
-            return;
-        }
-        final Iterator<PointerTracker> it = mQueue.iterator();
-        while (it.hasNext()) {
-            final PointerTracker 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(t.getLastX(), t.getLastY(), 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(PointerTracker 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.mPointerId + "] " + this);
+                Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this);
             }
         }
-        final Iterator<PointerTracker> it = mQueue.iterator();
-        while (it.hasNext()) {
-            final PointerTracker t = it.next();
-            if (t != tracker) {
-                t.onPhantomUpEvent(t.getLastX(), t.getLastY(), 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(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 (element.isModifier()) {
+                return true;
             }
         }
+        return false;
     }
 
     public synchronized boolean isAnyInSlidingKeyInput() {
-        for (final PointerTracker 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;
             }
         }
@@ -91,14 +183,16 @@
     }
 
     @Override
-    public String toString() {
+    public synchronized String toString() {
         final StringBuilder sb = new StringBuilder();
-        for (final PointerTracker 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.mPointerId + " "
-                + Keyboard.printableCode(tracker.getKey().mCode) + "]");
+            sb.append(element.toString());
         }
-        return sb.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
new file mode 100644
index 0000000..15170e0
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
@@ -0,0 +1,317 @@
+/*
+ * 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.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+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.Params;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+
+public class PreviewPlacerView extends RelativeLayout {
+    private final int mGestureFloatingPreviewTextColor;
+    private final int mGestureFloatingPreviewTextOffset;
+    private final int mGestureFloatingPreviewColor;
+    private final float mGestureFloatingPreviewHorizontalPadding;
+    private final float mGestureFloatingPreviewVerticalPadding;
+    private final float mGestureFloatingPreviewRoundRadius;
+
+    private int mXOrigin;
+    private int mYOrigin;
+
+    private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails =
+            CollectionUtils.newSparseArray();
+    private final Params mGesturePreviewTrailParams;
+    private final Paint mGesturePaint;
+    private boolean mDrawsGesturePreviewTrail;
+    private Bitmap mOffscreenBuffer;
+    private final Canvas mOffscreenCanvas = new Canvas();
+    private final Rect mOffscreenDirtyRect = new Rect();
+    private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail
+
+    private final Paint mTextPaint;
+    private String mGestureFloatingPreviewText;
+    private final int mGestureFloatingPreviewTextHeight;
+    // {@link RectF} is needed for {@link Canvas#drawRoundRect(RectF, float, float, Paint)}.
+    private final RectF mGestureFloatingPreviewRectangle = new RectF();
+    private int mLastPointerX;
+    private int mLastPointerY;
+    private static final char[] TEXT_HEIGHT_REFERENCE_CHAR = { 'M' };
+    private boolean mDrawsGestureFloatingPreviewText;
+
+    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;
+
+        private final Params mGesturePreviewTrailParams;
+        private final int mGestureFloatingPreviewTextLingerTimeout;
+
+        public DrawingHandler(final PreviewPlacerView outerInstance,
+                final Params gesturePreviewTrailParams,
+                final int getstureFloatinPreviewTextLinerTimeout) {
+            super(outerInstance);
+            mGesturePreviewTrailParams = gesturePreviewTrailParams;
+            mGestureFloatingPreviewTextLingerTimeout = getstureFloatinPreviewTextLinerTimeout;
+        }
+
+        @Override
+        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;
+            }
+        }
+
+        private void cancelDismissGestureFloatingPreviewText() {
+            removeMessages(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
+        }
+
+        public void dismissGestureFloatingPreviewText() {
+            cancelDismissGestureFloatingPreviewText();
+            sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT),
+                    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() {
+            cancelUpdateGestureTrailPreview();
+        }
+    }
+
+    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(
+                R.styleable.KeyboardView_gestureFloatingPreviewTextColor, 0);
+        mGestureFloatingPreviewTextOffset = keyboardViewAttr.getDimensionPixelOffset(
+                R.styleable.KeyboardView_gestureFloatingPreviewTextOffset, 0);
+        mGestureFloatingPreviewColor = keyboardViewAttr.getColor(
+                R.styleable.KeyboardView_gestureFloatingPreviewColor, 0);
+        mGestureFloatingPreviewHorizontalPadding = keyboardViewAttr.getDimension(
+                R.styleable.KeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f);
+        mGestureFloatingPreviewVerticalPadding = keyboardViewAttr.getDimension(
+                R.styleable.KeyboardView_gestureFloatingPreviewVerticalPadding, 0.0f);
+        mGestureFloatingPreviewRoundRadius = keyboardViewAttr.getDimension(
+                R.styleable.KeyboardView_gestureFloatingPreviewRoundRadius, 0.0f);
+        final int gestureFloatingPreviewTextLingerTimeout = keyboardViewAttr.getInt(
+                R.styleable.KeyboardView_gestureFloatingPreviewTextLingerTimeout, 0);
+        mGesturePreviewTrailParams = new Params(keyboardViewAttr);
+        keyboardViewAttr.recycle();
+
+        mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams,
+                gestureFloatingPreviewTextLingerTimeout);
+
+        final Paint gesturePaint = new Paint();
+        gesturePaint.setAntiAlias(true);
+        gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+        mGesturePaint = gesturePaint;
+
+        final Paint textPaint = new Paint();
+        textPaint.setAntiAlias(true);
+        textPaint.setTextAlign(Align.CENTER);
+        textPaint.setTextSize(gestureFloatingPreviewTextSize);
+        mTextPaint = textPaint;
+        final Rect textRect = new Rect();
+        textPaint.getTextBounds(TEXT_HEIGHT_REFERENCE_CHAR, 0, 1, textRect);
+        mGestureFloatingPreviewTextHeight = textRect.height();
+
+        final Paint layerPaint = new Paint();
+        layerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
+        setLayerType(LAYER_TYPE_HARDWARE, layerPaint);
+    }
+
+    public void setOrigin(final int x, final int y) {
+        mXOrigin = x;
+        mYOrigin = y;
+    }
+
+    public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+            final boolean drawsGestureFloatingPreviewText) {
+        mDrawsGesturePreviewTrail = drawsGesturePreviewTrail;
+        mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText;
+    }
+
+    public void invalidatePointer(final PointerTracker tracker, final boolean isOldestTracker) {
+        final boolean needsToUpdateLastPointer =
+                isOldestTracker && mDrawsGestureFloatingPreviewText;
+        if (needsToUpdateLastPointer) {
+            mLastPointerX = tracker.getLastX();
+            mLastPointerY = tracker.getLastY();
+        }
+
+        if (mDrawsGesturePreviewTrail) {
+            GesturePreviewTrail trail;
+            synchronized (mGesturePreviewTrails) {
+                trail = mGesturePreviewTrails.get(tracker.mPointerId);
+                if (trail == null) {
+                    trail = new GesturePreviewTrail();
+                    mGesturePreviewTrails.put(tracker.mPointerId, trail);
+                }
+            }
+            trail.addStroke(tracker.getGestureStrokeWithPreviewPoints(), tracker.getDownTime());
+        }
+
+        // TODO: Should narrow the invalidate region.
+        if (mDrawsGesturePreviewTrail || needsToUpdateLastPointer) {
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        if (mOffscreenBuffer != null) {
+            mOffscreenBuffer.recycle();
+            mOffscreenBuffer = null;
+        }
+    }
+
+    @Override
+    public void onDraw(final Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.translate(mXOrigin, mYOrigin);
+        if (mDrawsGesturePreviewTrail) {
+            if (mOffscreenBuffer == null) {
+                mOffscreenBuffer = Bitmap.createBitmap(
+                        getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
+                mOffscreenCanvas.setBitmap(mOffscreenBuffer);
+            }
+            if (!mOffscreenDirtyRect.isEmpty()) {
+                // Clear previous dirty rectangle.
+                mGesturePaint.setColor(Color.TRANSPARENT);
+                mGesturePaint.setStyle(Paint.Style.FILL);
+                mOffscreenCanvas.drawRect(mOffscreenDirtyRect, mGesturePaint);
+                mOffscreenDirtyRect.setEmpty();
+            }
+            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(mOffscreenCanvas, mGesturePaint,
+                                    mGesturePreviewTrailBoundsRect, mGesturePreviewTrailParams);
+                    // {@link #mGesturePreviewTrailBoundsRect} has bounding box of the trail.
+                    mOffscreenDirtyRect.union(mGesturePreviewTrailBoundsRect);
+                }
+            }
+            if (!mOffscreenDirtyRect.isEmpty()) {
+                canvas.drawBitmap(mOffscreenBuffer, mOffscreenDirtyRect, mOffscreenDirtyRect,
+                        mGesturePaint);
+                // Note: Defer clearing the dirty rectangle here because we will get cleared
+                // rectangle on the canvas.
+            }
+            if (needsUpdatingGesturePreviewTrail) {
+                mDrawingHandler.postUpdateGestureTrailPreview();
+            }
+        }
+        if (mDrawsGestureFloatingPreviewText) {
+            drawGestureFloatingPreviewText(canvas, mGestureFloatingPreviewText);
+        }
+        canvas.translate(-mXOrigin, -mYOrigin);
+    }
+
+    public void setGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
+        if (!mDrawsGestureFloatingPreviewText) return;
+        mGestureFloatingPreviewText = gestureFloatingPreviewText;
+        invalidate();
+    }
+
+    public void dismissGestureFloatingPreviewText() {
+        mDrawingHandler.dismissGestureFloatingPreviewText();
+    }
+
+    public void cancelAllMessages() {
+        mDrawingHandler.cancelAllMessages();
+    }
+
+    private void drawGestureFloatingPreviewText(final Canvas canvas,
+            final String gestureFloatingPreviewText) {
+        if (TextUtils.isEmpty(gestureFloatingPreviewText)) {
+            return;
+        }
+
+        final Paint paint = mTextPaint;
+        final RectF rectangle = mGestureFloatingPreviewRectangle;
+        // TODO: Figure out how we should deal with the floating preview text with multiple moving
+        // fingers.
+
+        // Paint the round rectangle background.
+        final int textHeight = mGestureFloatingPreviewTextHeight;
+        final float textWidth = paint.measureText(gestureFloatingPreviewText);
+        final float hPad = mGestureFloatingPreviewHorizontalPadding;
+        final float vPad = mGestureFloatingPreviewVerticalPadding;
+        final float rectWidth = textWidth + hPad * 2.0f;
+        final float rectHeight = textHeight + vPad * 2.0f;
+        final int canvasWidth = canvas.getWidth();
+        final float rectX = Math.min(Math.max(mLastPointerX - rectWidth / 2.0f, 0.0f),
+                canvasWidth - rectWidth);
+        final float rectY = mLastPointerY - mGestureFloatingPreviewTextOffset - rectHeight;
+        rectangle.set(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
+        final float round = mGestureFloatingPreviewRoundRadius;
+        paint.setColor(mGestureFloatingPreviewColor);
+        canvas.drawRoundRect(rectangle, round, round, paint);
+        // Paint the text preview
+        paint.setColor(mGestureFloatingPreviewTextColor);
+        final float textX = rectX + hPad + textWidth / 2.0f;
+        final float textY = rectY + vPad + textHeight;
+        canvas.drawText(gestureFloatingPreviewText, textX, textY, paint);
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java
similarity index 93%
rename from java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
rename to java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java
index 1071383..a591a7a 100644
--- a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java
@@ -14,17 +14,19 @@
  * the License.
  */
 
-package com.android.inputmethod.keyboard;
+package com.android.inputmethod.keyboard.internal;
 
 import android.content.Context;
 import android.util.Log;
 import android.view.MotionEvent;
 
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.MainKeyboardView;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResearchLogger;
-import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.ResourceUtils;
 import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
 
 public class SuddenJumpingTouchEventHandler {
     private static final String TAG = SuddenJumpingTouchEventHandler.class.getSimpleName();
@@ -51,7 +53,7 @@
 
     public SuddenJumpingTouchEventHandler(Context context, ProcessMotionEvent view) {
         mView = view;
-        mNeedsSuddenJumpingHack = Boolean.parseBoolean(Utils.getDeviceOverrideValue(
+        mNeedsSuddenJumpingHack = Boolean.parseBoolean(ResourceUtils.getDeviceOverrideValue(
                 context.getResources(), R.array.sudden_jumping_touch_event_device_list, "false"));
     }
 
@@ -70,7 +72,7 @@
      * the sudden moves subside, a DOWN event is simulated for the second key.
      * @param me the motion event
      * @return true if the event was consumed, so that it doesn't continue to be handled by
-     * {@link LatinKeyboardView}.
+     * {@link MainKeyboardView}.
      */
     private boolean handleSuddenJumping(MotionEvent me) {
         if (!mNeedsSuddenJumpingHack)
diff --git a/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java b/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java
new file mode 100644
index 0000000..69dc01c
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java
@@ -0,0 +1,76 @@
+/*
+ * 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.LatinImeLogger;
+
+public class TouchPositionCorrection {
+    private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3;
+
+    public boolean mEnabled;
+    public float[] mXs;
+    public float[] mYs;
+    public float[] mRadii;
+
+    public void load(final String[] data) {
+        final int dataLength = data.length;
+        if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) {
+            if (LatinImeLogger.sDBG) {
+                throw new RuntimeException(
+                        "the size of touch position correction data is invalid");
+            }
+            return;
+        }
+
+        final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+        mXs = new float[length];
+        mYs = new float[length];
+        mRadii = new float[length];
+        try {
+            for (int i = 0; i < dataLength; ++i) {
+                final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+                final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+                final float value = Float.parseFloat(data[i]);
+                if (type == 0) {
+                    mXs[index] = value;
+                } else if (type == 1) {
+                    mYs[index] = value;
+                } else {
+                    mRadii[index] = value;
+                }
+            }
+        } catch (NumberFormatException e) {
+            if (LatinImeLogger.sDBG) {
+                throw new RuntimeException(
+                        "the number format for touch position correction data is invalid");
+            }
+            mXs = null;
+            mYs = null;
+            mRadii = null;
+        }
+    }
+
+    // TODO: Remove this method.
+    public void setEnabled(final boolean enabled) {
+        mEnabled = enabled;
+    }
+
+    public boolean isValid() {
+        return mEnabled && mXs != null && mYs != null && mRadii != null
+                && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
index f8f1395..509fc1b 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
@@ -27,22 +27,22 @@
 
 import java.util.ArrayList;
 
-public class AdditionalSubtype {
+public final class AdditionalSubtype {
     private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0];
 
     private AdditionalSubtype() {
         // This utility class is not publicly instantiable.
     }
 
-    public static boolean isAdditionalSubtype(InputMethodSubtype subtype) {
+    public static boolean isAdditionalSubtype(final InputMethodSubtype subtype) {
         return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE);
     }
 
     private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":";
-    public static final String PREF_SUBTYPE_SEPARATOR = ";";
+    private static final String PREF_SUBTYPE_SEPARATOR = ";";
 
-    public static InputMethodSubtype createAdditionalSubtype(
-            String localeString, String keyboardLayoutSetName, String extraValue) {
+    public static InputMethodSubtype createAdditionalSubtype(final String localeString,
+            final String keyboardLayoutSetName, final String extraValue) {
         final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
         final String layoutDisplayNameExtraValue;
         if (Build.VERSION.SDK_INT >= /* JELLY_BEAN */ 15
@@ -62,7 +62,7 @@
                 layoutExtraValue + "," + additionalSubtypeExtraValue, false, false);
     }
 
-    public static String getPrefSubtype(InputMethodSubtype subtype) {
+    public static String getPrefSubtype(final InputMethodSubtype subtype) {
         final String localeString = subtype.getLocale();
         final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
         final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
@@ -74,7 +74,7 @@
                 : basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue;
     }
 
-    public static InputMethodSubtype createAdditionalSubtype(String prefSubtype) {
+    public static InputMethodSubtype createAdditionalSubtype(final String prefSubtype) {
         final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
         if (elems.length < 2 || elems.length > 3) {
             throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype);
@@ -85,13 +85,13 @@
         return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue);
     }
 
-    public static InputMethodSubtype[] createAdditionalSubtypesArray(String prefSubtypes) {
+    public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) {
         if (TextUtils.isEmpty(prefSubtypes)) {
             return EMPTY_SUBTYPE_ARRAY;
         }
         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) {
@@ -103,4 +103,32 @@
         }
         return subtypesList.toArray(new InputMethodSubtype[subtypesList.size()]);
     }
+
+    public static String createPrefSubtypes(final InputMethodSubtype[] subtypes) {
+        if (subtypes == null || subtypes.length == 0) {
+            return "";
+        }
+        final StringBuilder sb = new StringBuilder();
+        for (final InputMethodSubtype subtype : subtypes) {
+            if (sb.length() > 0) {
+                sb.append(PREF_SUBTYPE_SEPARATOR);
+            }
+            sb.append(getPrefSubtype(subtype));
+        }
+        return sb.toString();
+    }
+
+    public static String createPrefSubtypes(final String[] prefSubtypes) {
+        if (prefSubtypes == null || prefSubtypes.length == 0) {
+            return "";
+        }
+        final StringBuilder sb = new StringBuilder();
+        for (final String prefSubtype : prefSubtypes) {
+            if (sb.length() > 0) {
+                sb.append(PREF_SUBTYPE_SEPARATOR);
+            }
+            sb.append(prefSubtype);
+        }
+        return sb.toString();
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
index 779a388..ae51d25 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
@@ -63,13 +63,13 @@
     private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN =
             "is_subtype_enabler_notification_dialog_open";
     private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler";
-    static class SubtypeLocaleItem extends Pair<String, String>
+    static final class SubtypeLocaleItem extends Pair<String, String>
             implements Comparable<SubtypeLocaleItem> {
-        public SubtypeLocaleItem(String localeString, String displayName) {
+        public SubtypeLocaleItem(final String localeString, final String displayName) {
             super(localeString, displayName);
         }
 
-        public SubtypeLocaleItem(String localeString) {
+        public SubtypeLocaleItem(final String localeString) {
             this(localeString, SubtypeLocale.getSubtypeLocaleDisplayName(localeString));
         }
 
@@ -79,17 +79,17 @@
         }
 
         @Override
-        public int compareTo(SubtypeLocaleItem o) {
+        public int compareTo(final SubtypeLocaleItem o) {
             return first.compareTo(o.first);
         }
     }
 
-    static class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
-        public SubtypeLocaleAdapter(Context context) {
+    static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
+        public SubtypeLocaleAdapter(final Context context) {
             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++) {
@@ -102,7 +102,8 @@
             addAll(items);
         }
 
-        public static SubtypeLocaleItem createItem(Context context, String localeString) {
+        public static SubtypeLocaleItem createItem(final Context context,
+                final String localeString) {
             if (localeString.equals(SubtypeLocale.NO_LANGUAGE)) {
                 final String displayName = context.getString(R.string.subtype_no_language);
                 return new SubtypeLocaleItem(localeString, displayName);
@@ -112,8 +113,8 @@
         }
     }
 
-    static class KeyboardLayoutSetItem extends Pair<String, String> {
-        public KeyboardLayoutSetItem(InputMethodSubtype subtype) {
+    static final class KeyboardLayoutSetItem extends Pair<String, String> {
+        public KeyboardLayoutSetItem(final InputMethodSubtype subtype) {
             super(SubtypeLocale.getKeyboardLayoutSetName(subtype),
                     SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype));
         }
@@ -124,8 +125,8 @@
         }
     }
 
-    static class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
-        public KeyboardLayoutSetAdapter(Context context) {
+    static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
+        public KeyboardLayoutSetAdapter(final Context context) {
             super(context, android.R.layout.simple_spinner_item);
             setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 
@@ -147,7 +148,7 @@
         public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter();
     }
 
-    static class SubtypePreference extends DialogPreference
+    static final class SubtypePreference extends DialogPreference
             implements DialogInterface.OnCancelListener {
         private static final String KEY_PREFIX = "subtype_pref_";
         private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new";
@@ -159,13 +160,13 @@
         private Spinner mSubtypeLocaleSpinner;
         private Spinner mKeyboardLayoutSetSpinner;
 
-        public static SubtypePreference newIncompleteSubtypePreference(
-                Context context, SubtypeDialogProxy proxy) {
+        public static SubtypePreference newIncompleteSubtypePreference(final Context context,
+                final SubtypeDialogProxy proxy) {
             return new SubtypePreference(context, null, proxy);
         }
 
-        public SubtypePreference(Context context, InputMethodSubtype subtype,
-                SubtypeDialogProxy proxy) {
+        public SubtypePreference(final Context context, final InputMethodSubtype subtype,
+                final SubtypeDialogProxy proxy) {
             super(context, null);
             setDialogLayoutResource(R.layout.additional_subtype_dialog);
             setPersistent(false);
@@ -185,7 +186,7 @@
             return mSubtype;
         }
 
-        public void setSubtype(InputMethodSubtype subtype) {
+        public void setSubtype(final InputMethodSubtype subtype) {
             mPreviousSubtype = mSubtype;
             mSubtype = subtype;
             if (isIncomplete()) {
@@ -221,7 +222,7 @@
         }
 
         @Override
-        protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+        protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
             final Context context = builder.getContext();
             builder.setCancelable(true).setOnCancelListener(this);
             if (isIncomplete()) {
@@ -239,7 +240,7 @@
             }
         }
 
-        private static void setSpinnerPosition(Spinner spinner, Object itemToSelect) {
+        private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) {
             final SpinnerAdapter adapter = spinner.getAdapter();
             final int count = adapter.getCount();
             for (int i = 0; i < count; i++) {
@@ -252,14 +253,14 @@
         }
 
         @Override
-        public void onCancel(DialogInterface dialog) {
+        public void onCancel(final DialogInterface dialog) {
             if (isIncomplete()) {
                 mProxy.onRemovePressed(this);
             }
         }
 
         @Override
-        public void onClick(DialogInterface dialog, int which) {
+        public void onClick(final DialogInterface dialog, final int which) {
             super.onClick(dialog, which);
             switch (which) {
             case DialogInterface.BUTTON_POSITIVE:
@@ -287,12 +288,12 @@
             }
         }
 
-        private static int getSpinnerPosition(Spinner spinner) {
+        private static int getSpinnerPosition(final Spinner spinner) {
             if (spinner == null) return -1;
             return spinner.getSelectedItemPosition();
         }
 
-        private static void setSpinnerPosition(Spinner spinner, int position) {
+        private static void setSpinnerPosition(final Spinner spinner, final int position) {
             if (spinner == null || position < 0) return;
             spinner.setSelection(position);
         }
@@ -313,7 +314,7 @@
         }
 
         @Override
-        protected void onRestoreInstanceState(Parcelable state) {
+        protected void onRestoreInstanceState(final Parcelable state) {
             if (!(state instanceof SavedState)) {
                 super.onRestoreInstanceState(state);
                 return;
@@ -326,24 +327,24 @@
             setSubtype(myState.mSubtype);
         }
 
-        static class SavedState extends Preference.BaseSavedState {
+        static final class SavedState extends Preference.BaseSavedState {
             InputMethodSubtype mSubtype;
             int mSubtypeLocaleSelectedPos;
             int mKeyboardLayoutSetSelectedPos;
 
-            public SavedState(Parcelable superState) {
+            public SavedState(final Parcelable superState) {
                 super(superState);
             }
 
             @Override
-            public void writeToParcel(Parcel dest, int flags) {
+            public void writeToParcel(final Parcel dest, final int flags) {
                 super.writeToParcel(dest, flags);
                 dest.writeInt(mSubtypeLocaleSelectedPos);
                 dest.writeInt(mKeyboardLayoutSetSelectedPos);
                 dest.writeParcelable(mSubtype, 0);
             }
 
-            public SavedState(Parcel source) {
+            public SavedState(final Parcel source) {
                 super(source);
                 mSubtypeLocaleSelectedPos = source.readInt();
                 mKeyboardLayoutSetSelectedPos = source.readInt();
@@ -354,12 +355,12 @@
             public static final Parcelable.Creator<SavedState> CREATOR =
                     new Parcelable.Creator<SavedState>() {
                         @Override
-                        public SavedState createFromParcel(Parcel source) {
+                        public SavedState createFromParcel(final Parcel source) {
                             return new SavedState(source);
                         }
 
                         @Override
-                        public SavedState[] newArray(int size) {
+                        public SavedState[] newArray(final int size) {
                             return new SavedState[size];
                         }
                     };
@@ -371,7 +372,7 @@
     }
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
+    public void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
         addPreferencesFromResource(R.xml.additional_subtype_settings);
@@ -381,7 +382,7 @@
     }
 
     @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
+    public void onActivityCreated(final Bundle savedInstanceState) {
         final Context context = getActivity();
         mSubtypeLocaleAdapter = new SubtypeLocaleAdapter(context);
         mKeyboardLayoutSetAdapter = new KeyboardLayoutSetAdapter(context);
@@ -411,7 +412,7 @@
     }
 
     @Override
-    public void onSaveInstanceState(Bundle outState) {
+    public void onSaveInstanceState(final Bundle outState) {
         super.onSaveInstanceState(outState);
         if (mIsAddingNewSubtype) {
             outState.putBoolean(KEY_IS_ADDING_NEW_SUBTYPE, true);
@@ -426,7 +427,7 @@
 
     private final SubtypeDialogProxy mSubtypeProxy = new SubtypeDialogProxy() {
         @Override
-        public void onRemovePressed(SubtypePreference subtypePref) {
+        public void onRemovePressed(final SubtypePreference subtypePref) {
             mIsAddingNewSubtype = false;
             final PreferenceGroup group = getPreferenceScreen();
             group.removePreference(subtypePref);
@@ -434,7 +435,7 @@
         }
 
         @Override
-        public void onSavePressed(SubtypePreference subtypePref) {
+        public void onSavePressed(final SubtypePreference subtypePref) {
             final InputMethodSubtype subtype = subtypePref.getSubtype();
             if (!subtypePref.hasBeenModified()) {
                 return;
@@ -453,7 +454,7 @@
         }
 
         @Override
-        public void onAddPressed(SubtypePreference subtypePref) {
+        public void onAddPressed(final SubtypePreference subtypePref) {
             mIsAddingNewSubtype = false;
             final InputMethodSubtype subtype = subtypePref.getSubtype();
             if (findDuplicatedSubtype(subtype) == null) {
@@ -481,7 +482,7 @@
         }
     };
 
-    private void showSubtypeAlreadyExistsToast(InputMethodSubtype subtype) {
+    private void showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype) {
         final Context context = getActivity();
         final Resources res = context.getResources();
         final String message = res.getString(R.string.custom_input_style_already_exists,
@@ -489,14 +490,15 @@
         Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
     }
 
-    private InputMethodSubtype findDuplicatedSubtype(InputMethodSubtype subtype) {
+    private InputMethodSubtype findDuplicatedSubtype(final InputMethodSubtype subtype) {
         final String localeString = subtype.getLocale();
         final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
         return ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet(
                 getActivity(), localeString, keyboardLayoutSetName);
     }
 
-    private AlertDialog createDialog(SubtypePreference subtypePref) {
+    private AlertDialog createDialog(
+            @SuppressWarnings("unused") final SubtypePreference subtypePref) {
         final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
         builder.setTitle(R.string.custom_input_styles_title)
                 .setMessage(R.string.custom_input_style_note_message)
@@ -519,7 +521,7 @@
         return builder.create();
     }
 
-    private void setPrefSubtypes(String prefSubtypes, Context context) {
+    private void setPrefSubtypes(final String prefSubtypes, final Context context) {
         final PreferenceGroup group = getPreferenceScreen();
         group.removeAll();
         final InputMethodSubtype[] subtypesArray =
@@ -533,7 +535,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);
@@ -547,23 +549,12 @@
         return subtypes.toArray(new InputMethodSubtype[subtypes.size()]);
     }
 
-    private String getPrefSubtypes(InputMethodSubtype[] subtypes) {
-        final StringBuilder sb = new StringBuilder();
-        for (final InputMethodSubtype subtype : subtypes) {
-            if (sb.length() > 0) {
-                sb.append(AdditionalSubtype.PREF_SUBTYPE_SEPARATOR);
-            }
-            sb.append(AdditionalSubtype.getPrefSubtype(subtype));
-        }
-        return sb.toString();
-    }
-
     @Override
     public void onPause() {
         super.onPause();
         final String oldSubtypes = SettingsValues.getPrefAdditionalSubtypes(mPrefs, getResources());
         final InputMethodSubtype[] subtypes = getSubtypes();
-        final String prefSubtypes = getPrefSubtypes(subtypes);
+        final String prefSubtypes = AdditionalSubtype.createPrefSubtypes(subtypes);
         if (prefSubtypes.equals(oldSubtypes)) {
             return;
         }
@@ -578,13 +569,13 @@
     }
 
     @Override
-    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+    public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
         final MenuItem addSubtypeMenu = menu.add(0, MENU_ADD_SUBTYPE, 0, R.string.add_style);
         addSubtypeMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
     }
 
     @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
+    public boolean onOptionsItemSelected(final MenuItem item) {
         final int itemId = item.getItemId();
         if (itemId == MENU_ADD_SUBTYPE) {
             final SubtypePreference newSubtype =
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index e0452483..f425e36 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -21,34 +21,17 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import java.util.ArrayList;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class AutoCorrection {
     private static final boolean DBG = LatinImeLogger.sDBG;
     private static final String TAG = AutoCorrection.class.getSimpleName();
+    private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
 
     private AutoCorrection() {
         // Purely static class: can't instantiate.
     }
 
-    public static CharSequence computeAutoCorrectionWord(
-            final ConcurrentHashMap<String, Dictionary> dictionaries,
-            final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
-            final CharSequence consideredWord, final float autoCorrectionThreshold,
-            final CharSequence whitelistedWord) {
-        if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) {
-            return whitelistedWord;
-        } else if (hasAutoCorrectionForConsideredWord(
-                dictionaries, wordComposer, suggestions, consideredWord)) {
-            return consideredWord;
-        } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions,
-                consideredWord, autoCorrectionThreshold)) {
-            return suggestions.get(0).mWord;
-        }
-        return null;
-    }
-
     public static boolean isValidWord(final ConcurrentHashMap<String, Dictionary> dictionaries,
             CharSequence word, boolean ignoreCase) {
         if (TextUtils.isEmpty(word)) {
@@ -56,7 +39,6 @@
         }
         final CharSequence lowerCasedWord = word.toString().toLowerCase();
         for (final String key : dictionaries.keySet()) {
-            if (key.equals(Suggest.DICT_KEY_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
@@ -81,7 +63,6 @@
         }
         int maxFreq = -1;
         for (final String key : dictionaries.keySet()) {
-            if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
             final Dictionary dictionary = dictionaries.get(key);
             if (null == dictionary) continue;
             final int tempFreq = dictionary.getFrequency(word);
@@ -92,46 +73,26 @@
         return maxFreq;
     }
 
-    public static boolean allowsToBeAutoCorrected(
+    // Returns true if this is in any of the dictionaries.
+    public static boolean isInTheDictionary(
             final ConcurrentHashMap<String, Dictionary> dictionaries,
             final CharSequence word, final boolean ignoreCase) {
-        final WhitelistDictionary whitelistDictionary =
-                (WhitelistDictionary)dictionaries.get(Suggest.DICT_KEY_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);
+        return isValidWord(dictionaries, word, ignoreCase);
     }
 
-    private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) {
-        return whiteListedWord != null;
-    }
-
-    private static boolean hasAutoCorrectionForConsideredWord(
-            final ConcurrentHashMap<String, Dictionary> dictionaries,
-            final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
-            final CharSequence consideredWord) {
-        if (TextUtils.isEmpty(consideredWord)) return false;
-        return wordComposer.size() > 1 && suggestions.size() > 0
-                && !allowsToBeAutoCorrected(dictionaries, consideredWord, false);
-    }
-
-    private static boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer,
-            ArrayList<SuggestedWordInfo> suggestions,
+    public static boolean suggestionExceedsAutoCorrectionThreshold(SuggestedWordInfo suggestion,
             CharSequence consideredWord, float autoCorrectionThreshold) {
-        if (wordComposer.size() > 1 && suggestions.size() > 0) {
-            final SuggestedWordInfo autoCorrectionSuggestion = suggestions.get(0);
-            //final int autoCorrectionSuggestionScore = sortedScores[0];
-            final int autoCorrectionSuggestionScore = autoCorrectionSuggestion.mScore;
+        if (null != suggestion) {
+            // Shortlist a whitelisted word
+            if (suggestion.mKind == SuggestedWordInfo.KIND_WHITELIST) return true;
+            final int autoCorrectionSuggestionScore = suggestion.mScore;
             // TODO: when the normalized score of the first suggestion is nearly equals to
             //       the normalized score of the second suggestion, behave less aggressive.
             final float normalizedScore = BinaryDictionary.calcNormalizedScore(
-                    consideredWord.toString(), autoCorrectionSuggestion.mWord.toString(),
+                    consideredWord.toString(), suggestion.mWord.toString(),
                     autoCorrectionSuggestionScore);
             if (DBG) {
-                Log.d(TAG, "Normalized " + consideredWord + "," + autoCorrectionSuggestion + ","
+                Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
                         + autoCorrectionSuggestionScore + ", " + normalizedScore
                         + "(" + autoCorrectionThreshold + ")");
             }
@@ -139,10 +100,43 @@
                 if (DBG) {
                     Log.d(TAG, "Auto corrected by S-threshold.");
                 }
-                return true;
+                return !shouldBlockAutoCorrectionBySafetyNet(consideredWord.toString(),
+                        suggestion.mWord);
             }
         }
         return false;
     }
 
+    // TODO: Resolve the inconsistencies between the native auto correction algorithms and
+    // this safety net
+    public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
+            final CharSequence suggestion) {
+        // Safety net for auto correction.
+        // Actually if we hit this safety net, it's a bug.
+        // If user selected aggressive auto correction mode, there is no need to use the safety
+        // net.
+        // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
+        // we should not use net because relatively edit distance can be big.
+        final int typedWordLength = typedWord.length();
+        if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) {
+            return false;
+        }
+        final int maxEditDistanceOfNativeDictionary =
+                (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
+        final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
+        if (DBG) {
+            Log.d(TAG, "Autocorrected edit distance = " + distance
+                    + ", " + maxEditDistanceOfNativeDictionary);
+        }
+        if (distance > maxEditDistanceOfNativeDictionary) {
+            if (DBG) {
+                Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
+                Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
+                        + "Turning off auto-correction.");
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index d0613bd..9244f16 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -18,9 +18,12 @@
 
 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;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Locale;
 
@@ -38,24 +41,46 @@
      * It is necessary to keep it at this value because some languages e.g. German have
      * really long words.
      */
-    public static final int MAX_WORD_LENGTH = 48;
+    public static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
     public static final int MAX_WORDS = 18;
+    public static final int MAX_SPACES = 16;
 
-    private static final String TAG = "BinaryDictionary";
-    private static final int MAX_BIGRAMS = 60;
+    private static final String TAG = BinaryDictionary.class.getSimpleName();
+    private static final int MAX_PREDICTIONS = 60;
+    private static final int MAX_RESULTS = Math.max(MAX_PREDICTIONS, MAX_WORDS);
 
     private static final int TYPED_LETTER_MULTIPLIER = 2;
 
-    private int mDicTypeId;
     private long mNativeDict;
-    private final int[] mInputCodes = new int[MAX_WORD_LENGTH];
-    private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
-    private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
-    private final int[] mScores = new int[MAX_WORDS];
-    private final int[] mBigramScores = new int[MAX_BIGRAMS];
+    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];
+    private final int[] mOutputTypes = new int[MAX_RESULTS];
 
     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.
@@ -65,14 +90,13 @@
      * @param offset the offset of the dictionary data within the file.
      * @param length the length of the binary data.
      * @param useFullEditDistance whether to use the full edit distance in suggestions
+     * @param dictType the dictionary type, as a human-readable string
      */
     public BinaryDictionary(final Context context,
             final String filename, final long offset, final long length,
-            final boolean useFullEditDistance, final Locale locale) {
-        // Note: at the moment a binary dictionary is always of the "main" type.
-        // Initializing this here will help transitioning out of the scheme where
-        // the Suggest class knows everything about every single dictionary.
-        mDicTypeId = Suggest.DIC_MAIN;
+            final boolean useFullEditDistance, final Locale locale, final String dictType) {
+        super(dictType);
+        mLocale = locale;
         mUseFullEditDistance = useFullEditDistance;
         loadDictionary(filename, offset, length);
     }
@@ -82,121 +106,88 @@
     }
 
     private native long openNative(String sourceDir, long dictOffset, long dictSize,
-            int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords);
+            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[] inputCodes, int codesSize, int[] prevWordForBigrams,
-            boolean useFullEditDistance, char[] outputChars, int[] scores);
-    private native int getBigramsNative(long dict, int[] prevWord, int prevWordLength,
-            int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores,
-            int maxWordLength, int maxBigrams);
-    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 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, 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);
+        mNativeDict = openNative(path, startOffset, length, TYPED_LETTER_MULTIPLIER,
+                FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS, MAX_PREDICTIONS);
     }
 
     @Override
-    public void getBigrams(final WordComposer codes, final CharSequence previousWord,
-            final WordCallback callback) {
-        if (mNativeDict == 0) return;
-
-        int[] codePoints = StringUtils.toCodePointArray(previousWord.toString());
-        Arrays.fill(mOutputChars_bigrams, (char) 0);
-        Arrays.fill(mBigramScores, 0);
-
-        int codesSize = codes.size();
-        Arrays.fill(mInputCodes, -1);
-        if (codesSize > 0) {
-            mInputCodes[0] = codes.getCodeAt(0);
-        }
-
-        int count = getBigramsNative(mNativeDict, codePoints, codePoints.length, mInputCodes,
-                codesSize, mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS);
-        if (count > MAX_BIGRAMS) {
-            count = MAX_BIGRAMS;
-        }
-
-        for (int j = 0; j < count; ++j) {
-            if (codesSize > 0 && mBigramScores[j] < 1) break;
-            final int start = j * MAX_WORD_LENGTH;
-            int len = 0;
-            while (len <  MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) {
-                ++len;
-            }
-            if (len > 0) {
-                callback.addWord(mOutputChars_bigrams, start, len, mBigramScores[j],
-                        mDicTypeId, Dictionary.BIGRAM);
-            }
-        }
+    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo) {
+        return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, 0);
     }
 
-    // proximityInfo and/or prevWordForBigrams may not be null.
     @Override
-    public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
-            final WordCallback callback, final ProximityInfo proximityInfo) {
-        final int count = getSuggestions(codes, prevWordForBigrams, proximityInfo, mOutputChars,
-                mScores);
+    public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) {
+        if (!isValidDictionary()) return null;
 
+        Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
+        // TODO: toLowerCase in the native code
+        final int[] prevWordCodePointArray = (null == prevWord)
+                ? null : StringUtils.toCodePointArray(prevWord.toString());
+        final int composerSize = composer.size();
+
+        final boolean isGesture = composer.isBatchMode();
+        if (composerSize <= 1 || !isGesture) {
+            if (composerSize > MAX_WORD_LENGTH - 1) return null;
+            for (int i = 0; i < composerSize; i++) {
+                mInputCodePoints[i] = composer.getCodeAt(i);
+            }
+        }
+
+        final InputPointers ips = composer.getInputPointers();
+        final int codesSize = isGesture ? ips.getPointerSize() : composerSize;
+        // proximityInfo and/or prevWordForBigrams may not be null.
+        final int tmpCount = getSuggestionsNative(mNativeDict,
+                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 = CollectionUtils.newArrayList();
         for (int j = 0; j < count; ++j) {
-            if (mScores[j] < 1) break;
+            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) {
                 ++len;
             }
             if (len > 0) {
-                callback.addWord(mOutputChars, start, len, mScores[j], mDicTypeId,
-                        Dictionary.UNIGRAM);
+                final int score = SuggestedWordInfo.KIND_WHITELIST == mOutputTypes[j]
+                        ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j];
+                suggestions.add(new SuggestedWordInfo(
+                        new String(mOutputChars, start, len), score, mOutputTypes[j], mDictType));
             }
         }
+        return suggestions;
     }
 
     /* package for test */ boolean isValidDictionary() {
         return mNativeDict != 0;
     }
 
-    // proximityInfo may not be null.
-    /* package for test */ int getSuggestions(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo,
-            char[] outputChars, int[] scores) {
-        if (!isValidDictionary()) return -1;
-
-        final int codesSize = codes.size();
-        // Won't deal with really long words.
-        if (codesSize > MAX_WORD_LENGTH - 1) return -1;
-
-        Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE);
-        for (int i = 0; i < codesSize; i++) {
-            mInputCodes[i] = codes.getCodeAt(i);
-        }
-        Arrays.fill(outputChars, (char) 0);
-        Arrays.fill(scores, 0);
-
-        final int[] prevWordCodePointArray = null == prevWordForBigrams
-                ? null : StringUtils.toCodePointArray(prevWordForBigrams.toString());
-
-        // TODO: pass the previous word to native code
-        return getSuggestionsNative(
-                mNativeDict, proximityInfo.getNativeProximityInfo(),
-                codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize,
-                prevWordCodePointArray, mUseFullEditDistance, outputChars, scores);
-    }
-
     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
@@ -207,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
@@ -221,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..9764df0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -16,6 +16,9 @@
 
 package com.android.inputmethod.latin;
 
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FormatSpec;
+
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -23,6 +26,10 @@
 import android.util.Log;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Locale;
@@ -51,6 +58,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 +264,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 +345,57 @@
         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 BinaryDictInputOutput.ByteBufferWrapper buffer =
+                    new BinaryDictInputOutput.ByteBufferWrapper(inStream.getChannel().map(
+                            FileChannel.MapMode.READ_ONLY, 0, f.length()));
+            final int magic = buffer.readInt();
+            if (magic != FormatSpec.VERSION_2_MAGIC_NUMBER) {
+                return false;
+            }
+            final int formatVersion = buffer.readInt();
+            final int headerSize = buffer.readInt();
+            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;
+        } catch (BufferUnderflowException 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 +422,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/BoundedTreeSet.java b/java/src/com/android/inputmethod/latin/BoundedTreeSet.java
new file mode 100644
index 0000000..cf97761
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/BoundedTreeSet.java
@@ -0,0 +1,49 @@
+/*
+ * 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.SuggestedWords.SuggestedWordInfo;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+/**
+ * A TreeSet that is bounded in size and throws everything that's smaller than its limit
+ */
+public class BoundedTreeSet extends TreeSet<SuggestedWordInfo> {
+    private final int mCapacity;
+    public BoundedTreeSet(final Comparator<SuggestedWordInfo> comparator, final int capacity) {
+        super(comparator);
+        mCapacity = capacity;
+    }
+
+    @Override
+    public boolean add(final SuggestedWordInfo e) {
+        if (size() < mCapacity) return super.add(e);
+        if (comparator().compare(e, last()) > 0) return false;
+        super.add(e);
+        pollLast(); // removes the last element
+        return true;
+    }
+
+    @Override
+    public boolean addAll(final Collection<? extends SuggestedWordInfo> e) {
+        if (null == e) return false;
+        return super.addAll(e);
+    }
+}
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..c75f2df
--- /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 final 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 e79db36..57e12a6 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -16,9 +16,14 @@
 
 package com.android.inputmethod.latin;
 
-import android.view.inputmethod.EditorInfo;
-
 public final class Constants {
+    public static final class Color {
+        /**
+         * The alpha value for fully opaque.
+         */
+        public final static int ALPHA_OPAQUE = 255;
+    }
+
     public static final class ImeOption {
         /**
          * The private IME option used to indicate that no microphone should be shown for a given
@@ -47,7 +52,7 @@
          * The private IME option used to indicate that the given text field needs ASCII code points
          * input.
          *
-         * @deprecated Use {@link EditorInfo#IME_FLAG_FORCE_ASCII}.
+         * @deprecated Use EditorInfo#IME_FLAG_FORCE_ASCII.
          */
         @SuppressWarnings("dep-ann")
         public static final String FORCE_ASCII = "forceAscii";
@@ -121,6 +126,21 @@
         }
     }
 
+    public static class Dictionary {
+        public static final int MAX_WORD_LENGTH = 48;
+
+        private Dictionary() {
+             // This utility class is no publicly instantiable.
+        }
+    }
+
+    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/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 34308df..5edc431 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -52,6 +52,9 @@
     /** The number of contacts in the most recent dictionary rebuild. */
     static private int sContactCountAtLastRebuild = 0;
 
+    /** The locale for this contacts dictionary. Controls name bigram predictions. */
+    public final Locale mLocale;
+
     private ContentObserver mObserver;
 
     /**
@@ -59,8 +62,9 @@
      */
     private final boolean mUseFirstLastBigrams;
 
-    public ContactsBinaryDictionary(final Context context, final int dicTypeId, Locale locale) {
-        super(context, getFilenameWithLocale(NAME, locale.toString()), dicTypeId);
+    public ContactsBinaryDictionary(final Context context, Locale locale) {
+        super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS);
+        mLocale = locale;
         mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
         registerObserver(context);
 
@@ -116,12 +120,6 @@
         }
     }
 
-    @Override
-    public void getBigrams(final WordComposer codes, final CharSequence previousWord,
-            final WordCallback callback) {
-        super.getBigrams(codes, previousWord, callback);
-    }
-
     private boolean useFirstLastBigramsForLocale(Locale locale) {
         // TODO: Add firstname/lastname bigram rules for other languages.
         if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
@@ -163,7 +161,7 @@
      * bigrams depending on locale.
      */
     private void addName(String name) {
-        int len = name.codePointCount(0, name.length());
+        int len = StringUtils.codePointCount(name);
         String prevWord = null;
         // TODO: Better tokenization for non-Latin writing systems
         for (int i = 0; i < len; i++) {
@@ -173,7 +171,7 @@
                 i = end - 1;
                 // Don't add single letter words, possibly confuses
                 // capitalization of i.
-                final int wordLen = word.codePointCount(0, word.length());
+                final int wordLen = StringUtils.codePointCount(word);
                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
                     super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS);
                     if (!TextUtils.isEmpty(prevWord)) {
@@ -262,14 +260,14 @@
      * Checks if the words in a name are in the current binary dictionary.
      */
     private boolean isNameInDictionary(String name) {
-        int len = name.codePointCount(0, name.length());
+        int len = StringUtils.codePointCount(name);
         String prevWord = null;
         for (int i = 0; i < len; i++) {
             if (Character.isLetter(name.codePointAt(i))) {
                 int end = getWordEndPosition(name, len, i);
                 String word = name.substring(i, end);
                 i = end - 1;
-                final int wordLen = word.codePointCount(0, word.length());
+                final int wordLen = StringUtils.codePointCount(word);
                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
                     if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
                         if (!super.isValidBigramLocked(prevWord, word)) {
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
deleted file mode 100644
index cbfbd0e..0000000
--- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.keyboard.Keyboard;
-
-// TODO: This class is superseded by {@link ContactsBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words from Contacts provider.
- *
- * @deprecated Use {@link ContactsBinaryDictionary}.
- */
-@Deprecated
-public class ContactsDictionary extends ExpandableDictionary {
-
-    private static final String[] PROJECTION = {
-        BaseColumns._ID,
-        Contacts.DISPLAY_NAME,
-    };
-
-    private static final String TAG = "ContactsDictionary";
-
-    /**
-     * Frequency for contacts information into the dictionary
-     */
-    private static final int FREQUENCY_FOR_CONTACTS = 40;
-    private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
-
-    private static final int INDEX_NAME = 1;
-
-    private ContentObserver mObserver;
-
-    private long mLastLoadedContacts;
-
-    public ContactsDictionary(final Context context, final int dicTypeId) {
-        super(context, dicTypeId);
-        registerObserver(context);
-        loadDictionary();
-    }
-
-    private synchronized void registerObserver(final Context context) {
-        // Perform a managed query. The Activity will handle closing and requerying the cursor
-        // when needed.
-        if (mObserver != null) return;
-        ContentResolver cres = context.getContentResolver();
-        cres.registerContentObserver(
-                Contacts.CONTENT_URI, true, mObserver = new ContentObserver(null) {
-                    @Override
-                    public void onChange(boolean self) {
-                        setRequiresReload(true);
-                    }
-                });
-    }
-
-    public void reopen(final Context context) {
-        registerObserver(context);
-    }
-
-    @Override
-    public synchronized void close() {
-        if (mObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-            mObserver = null;
-        }
-        super.close();
-    }
-
-    @Override
-    public void startDictionaryLoadingTaskLocked() {
-        long now = SystemClock.uptimeMillis();
-        if (mLastLoadedContacts == 0
-                || now - mLastLoadedContacts > 30 * 60 * 1000 /* 30 minutes */) {
-            super.startDictionaryLoadingTaskLocked();
-        }
-    }
-
-    @Override
-    public void loadDictionaryAsync() {
-        try {
-            Cursor cursor = getContext().getContentResolver()
-                    .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
-            if (cursor != null) {
-                addWords(cursor);
-            }
-        } catch(IllegalStateException e) {
-            Log.e(TAG, "Contacts DB is having problems");
-        }
-        mLastLoadedContacts = SystemClock.uptimeMillis();
-    }
-
-    @Override
-    public void getBigrams(final WordComposer codes, final CharSequence previousWord,
-            final WordCallback callback) {
-        // Do not return bigrams from Contacts when nothing was typed.
-        if (codes.size() <= 0) return;
-        super.getBigrams(codes, previousWord, callback);
-    }
-
-    private void addWords(Cursor cursor) {
-        clearDictionary();
-
-        final int maxWordLength = getMaxWordLength();
-        try {
-            if (cursor.moveToFirst()) {
-                while (!cursor.isAfterLast()) {
-                    String name = cursor.getString(INDEX_NAME);
-
-                    if (name != null && -1 == name.indexOf('@')) {
-                        int len = name.length();
-                        String prevWord = null;
-
-                        // TODO: Better tokenization for non-Latin writing systems
-                        for (int i = 0; i < len; i++) {
-                            if (Character.isLetter(name.charAt(i))) {
-                                int j;
-                                for (j = i + 1; j < len; j++) {
-                                    char c = name.charAt(j);
-
-                                    if (!(c == Keyboard.CODE_DASH
-                                            || c == Keyboard.CODE_SINGLE_QUOTE
-                                            || Character.isLetter(c))) {
-                                        break;
-                                    }
-                                }
-
-                                String word = name.substring(i, j);
-                                i = j - 1;
-
-                                // Safeguard against adding really long words. Stack
-                                // may overflow due to recursion
-                                // Also don't add single letter words, possibly confuses
-                                // capitalization of i.
-                                final int wordLen = word.length();
-                                if (wordLen < maxWordLength && wordLen > 1) {
-                                    super.addWord(word, null /* shortcut */,
-                                            FREQUENCY_FOR_CONTACTS);
-                                    if (!TextUtils.isEmpty(prevWord)) {
-                                        super.setBigramAndGetFrequency(prevWord, word,
-                                                FREQUENCY_FOR_CONTACTS_BIGRAM);
-                                    }
-                                    prevWord = word;
-                                }
-                            }
-                        }
-                    }
-                    cursor.moveToNext();
-                }
-            }
-            cursor.close();
-        } catch(IllegalStateException e) {
-            Log.e(TAG, "Contacts DB is having problems");
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index af76498..1ea14da 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -23,10 +23,12 @@
 import android.os.Bundle;
 import android.os.Process;
 import android.preference.CheckBoxPreference;
+import android.preference.Preference;
 import android.preference.PreferenceFragment;
 import android.util.Log;
 
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.research.ResearchLogger;
 
 public class DebugSettings extends PreferenceFragment
         implements SharedPreferences.OnSharedPreferenceChangeListener {
@@ -34,6 +36,7 @@
     private static final String TAG = DebugSettings.class.getSimpleName();
     private static final String DEBUG_MODE_KEY = "debug_mode";
     public static final String FORCE_NON_DISTINCT_MULTITOUCH_KEY = "force_non_distinct_multitouch";
+    public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
 
     private boolean mServiceNeedsRestart = false;
     private CheckBoxPreference mDebugMode;
@@ -45,6 +48,14 @@
         SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
         prefs.registerOnSharedPreferenceChangeListener(this);
 
+        final Preference usabilityStudyPref = findPreference(PREF_USABILITY_STUDY_MODE);
+        if (usabilityStudyPref instanceof CheckBoxPreference) {
+            final CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
+            checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE,
+                    ResearchLogger.DEFAULT_USABILITY_STUDY_MODE));
+            checkbox.setSummary(R.string.settings_warning_researcher_mode);
+        }
+
         mServiceNeedsRestart = false;
         mDebugMode = (CheckBoxPreference) findPreference(DEBUG_MODE_KEY);
         updateDebugMode();
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 7cd9bc2..88d0c09 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -17,6 +17,9 @@
 package com.android.inputmethod.latin;
 
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.ArrayList;
 
 /**
  * Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
@@ -28,54 +31,41 @@
      */
     protected static final int FULL_WORD_SCORE_MULTIPLIER = 2;
 
-    public static final int UNIGRAM = 0;
-    public static final int BIGRAM = 1;
-
     public static final int NOT_A_PROBABILITY = -1;
-    /**
-     * Interface to be implemented by classes requesting words to be fetched from the dictionary.
-     * @see #getWords(WordComposer, CharSequence, WordCallback, ProximityInfo)
-     */
-    public interface WordCallback {
-        /**
-         * Adds a word to a list of suggestions. The word is expected to be ordered based on
-         * the provided score.
-         * @param word the character array containing the word
-         * @param wordOffset starting offset of the word in the character array
-         * @param wordLength length of valid characters in the character array
-         * @param score the score of occurrence. This is normalized between 1 and 255, but
-         * can exceed those limits
-         * @param dicTypeId of the dictionary where word was from
-         * @param dataType tells type of this data, either UNIGRAM or BIGRAM
-         * @return true if the word was added, false if no more words are required
-         */
-        boolean addWord(char[] word, int wordOffset, int wordLength, int score, int dicTypeId,
-                int dataType);
+
+    public static final String TYPE_USER_TYPED = "user_typed";
+    public static final String TYPE_APPLICATION_DEFINED = "application_defined";
+    public static final String TYPE_HARDCODED = "hardcoded"; // punctuation signs and such
+    public static final String TYPE_MAIN = "main";
+    public static final String TYPE_CONTACTS = "contacts";
+    // User dictionary, the system-managed one.
+    public static final String TYPE_USER = "user";
+    // User history dictionary internal to LatinIME.
+    public static final String TYPE_USER_HISTORY = "history";
+    protected final String mDictType;
+
+    public Dictionary(final String dictType) {
+        mDictType = dictType;
     }
 
     /**
-     * Searches for words in the dictionary that match the characters in the composer. Matched
-     * words are added through the callback object.
-     * @param composer the key sequence to match
-     * @param prevWordForBigrams the previous word, or null if none
-     * @param callback the callback object to send matched words to as possible candidates
+     * Searches for suggestions for a given context. For the moment the context is only the
+     * previous word.
+     * @param composer the key sequence to match with coordinate info, as a WordComposer
+     * @param prevWord the previous word, or null if none
      * @param proximityInfo the object for key proximity. May be ignored by some implementations.
-     * @see WordCallback#addWord(char[], int, int, int, int, int)
+     * @return the list of suggestions (possibly null if none)
      */
-    abstract public void getWords(final WordComposer composer,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo);
+    // TODO: pass more context than just the previous word, to enable better suggestions (n-gram
+    // and more)
+    abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo);
 
-    /**
-     * Searches for pairs in the bigram dictionary that matches the previous word and all the
-     * possible words following are added through the callback object.
-     * @param composer the key sequence to match
-     * @param previousWord the word before
-     * @param callback the callback object to send possible word following previous word
-     */
-    public void getBigrams(final WordComposer composer, final CharSequence previousWord,
-            final WordCallback callback) {
-        // empty base implementation
+    // 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);
     }
 
     /**
@@ -115,4 +105,12 @@
     public void close() {
         // empty base implementation
     }
+
+    /**
+     * Subclasses may override to indicate that this Dictionary is not yet properly initialized.
+     */
+
+    public boolean isInitialized() {
+        return true;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 1a05fcd..4acab6b 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -17,9 +17,11 @@
 package com.android.inputmethod.latin;
 
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 
 import android.util.Log;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -31,36 +33,44 @@
     private final String TAG = DictionaryCollection.class.getSimpleName();
     protected final CopyOnWriteArrayList<Dictionary> mDictionaries;
 
-    public DictionaryCollection() {
-        mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+    public DictionaryCollection(final String dictType) {
+        super(dictType);
+        mDictionaries = CollectionUtils.newCopyOnWriteArrayList();
     }
 
-    public DictionaryCollection(Dictionary... dictionaries) {
+    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(Collection<Dictionary> dictionaries) {
-        mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+    public DictionaryCollection(final String dictType, Collection<Dictionary> dictionaries) {
+        super(dictType);
+        mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries);
         mDictionaries.removeAll(Collections.singleton(null));
     }
 
     @Override
-    public void getWords(final WordComposer composer, final CharSequence prevWordForBigrams,
-            final WordCallback callback, final ProximityInfo proximityInfo) {
-        for (final Dictionary dict : mDictionaries)
-            dict.getWords(composer, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    @Override
-    public void getBigrams(final WordComposer composer, final CharSequence previousWord,
-            final WordCallback callback) {
-        for (final Dictionary dict : mDictionaries)
-            dict.getBigrams(composer, previousWord, callback);
+    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo) {
+        final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
+        if (dictionaries.isEmpty()) return null;
+        // To avoid creating unnecessary objects, we get the list out of the first
+        // 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 = CollectionUtils.newArrayList();
+        final int length = dictionaries.size();
+        for (int i = 1; i < length; ++ i) {
+            final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
+                    prevWord, proximityInfo);
+            if (null != sugg) suggestions.addAll(sugg);
+        }
+        return suggestions;
     }
 
     @Override
@@ -82,8 +92,9 @@
         return maxFreq;
     }
 
-    public boolean isEmpty() {
-        return mDictionaries.isEmpty();
+    @Override
+    public boolean isInitialized() {
+        return !mDictionaries.isEmpty();
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index a22d73a..cdd01d0 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -49,17 +49,18 @@
             final Locale locale, final boolean useFullEditDistance) {
         if (null == locale) {
             Log.e(TAG, "No locale defined for dictionary");
-            return new DictionaryCollection(createBinaryDictionary(context, locale));
+            return new DictionaryCollection(Dictionary.TYPE_MAIN,
+                    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) {
             for (final AssetFileAddress f : assetFileList) {
                 final BinaryDictionary binaryDictionary =
                         new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength,
-                                useFullEditDistance, locale);
+                                useFullEditDistance, locale, Dictionary.TYPE_MAIN);
                 if (binaryDictionary.isValidDictionary()) {
                     dictList.add(binaryDictionary);
                 }
@@ -69,7 +70,7 @@
         // If the list is empty, that means we should not use any dictionary (for example, the user
         // explicitly disabled the main dictionary), so the following is okay. dictList is never
         // null, but if for some reason it is, DictionaryCollection handles it gracefully.
-        return new DictionaryCollection(dictList);
+        return new DictionaryCollection(Dictionary.TYPE_MAIN, dictList);
     }
 
     /**
@@ -112,7 +113,7 @@
                 return null;
             }
             return new BinaryDictionary(context, sourceDir, afd.getStartOffset(), afd.getLength(),
-                    false /* useFullEditDistance */, locale);
+                    false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
         } catch (android.content.res.Resources.NotFoundException e) {
             Log.e(TAG, "Could not find the resource");
             return null;
@@ -140,7 +141,7 @@
             long startOffset, long length, final boolean useFullEditDistance, Locale locale) {
         if (dictionary.isFile()) {
             return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length,
-                    useFullEditDistance, locale);
+                    useFullEditDistance, locale, Dictionary.TYPE_MAIN);
         } else {
             Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
             return null;
diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
deleted file mode 100644
index 0f34d50..0000000
--- a/java/src/com/android/inputmethod/latin/EditingUtils.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-
-import java.util.regex.Pattern;
-
-/**
- * Utility methods to deal with editing text through an InputConnection.
- */
-public class EditingUtils {
-    /**
-     * Number of characters we want to look back in order to identify the previous word
-     */
-    // Provision for a long word pair and a separator
-    private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
-    private static final int INVALID_CURSOR_POSITION = -1;
-
-    private EditingUtils() {
-        // Unintentional empty constructor for singleton.
-    }
-
-    private static int getCursorPosition(InputConnection connection) {
-        if (null == connection) return INVALID_CURSOR_POSITION;
-        final ExtractedText extracted = connection.getExtractedText(new ExtractedTextRequest(), 0);
-        if (extracted == null) {
-            return INVALID_CURSOR_POSITION;
-        }
-        return extracted.startOffset + extracted.selectionStart;
-    }
-
-    /**
-     * @param connection connection to the current text field.
-     * @param separators characters which may separate words
-     * @return the word that surrounds the cursor, including up to one trailing
-     *   separator. For example, if the field contains "he|llo world", where |
-     *   represents the cursor, then "hello " will be returned.
-     */
-    public static String getWordAtCursor(InputConnection connection, String separators) {
-        // getWordRangeAtCursor returns null if the connection is null
-        Range r = getWordRangeAtCursor(connection, separators);
-        return (r == null) ? null : r.mWord;
-    }
-
-    /**
-     * Represents a range of text, relative to the current cursor position.
-     */
-    public static class Range {
-        /** Characters before selection start */
-        public final int mCharsBefore;
-
-        /**
-         * Characters after selection start, including one trailing word
-         * separator.
-         */
-        public final int mCharsAfter;
-
-        /** The actual characters that make up a word */
-        public final String mWord;
-
-        public Range(int charsBefore, int charsAfter, String word) {
-            if (charsBefore < 0 || charsAfter < 0) {
-                throw new IndexOutOfBoundsException();
-            }
-            this.mCharsBefore = charsBefore;
-            this.mCharsAfter = charsAfter;
-            this.mWord = word;
-        }
-    }
-
-    private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
-        if (connection == null || sep == null) {
-            return null;
-        }
-        CharSequence before = connection.getTextBeforeCursor(1000, 0);
-        CharSequence after = connection.getTextAfterCursor(1000, 0);
-        if (before == null || after == null) {
-            return null;
-        }
-
-        // Find first word separator before the cursor
-        int start = before.length();
-        while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
-
-        // Find last word separator after the cursor
-        int end = -1;
-        while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
-            // Nothing to do here.
-        }
-
-        int cursor = getCursorPosition(connection);
-        if (start >= 0 && cursor + end <= after.length() + before.length()) {
-            String word = before.toString().substring(start, before.length())
-                    + after.toString().substring(0, end);
-            return new Range(before.length() - start, end, word);
-        }
-
-        return null;
-    }
-
-    private static boolean isWhitespace(int code, String whitespace) {
-        return whitespace.contains(String.valueOf((char) code));
-    }
-
-    private static final Pattern spaceRegex = Pattern.compile("\\s+");
-
-    public static CharSequence getPreviousWord(InputConnection connection,
-            String sentenceSeperators) {
-        //TODO: Should fix this. This could be slow!
-        if (null == connection) return null;
-        CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
-        return getPreviousWord(prev, sentenceSeperators);
-    }
-
-    // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
-    // Also, it won't return words that end in a separator.
-    // Example :
-    // "abc def|" -> abc
-    // "abc def |" -> abc
-    // "abc def. |" -> abc
-    // "abc def . |" -> def
-    // "abc|" -> null
-    // "abc |" -> null
-    // "abc. def|" -> null
-    public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
-        if (prev == null) return null;
-        String[] w = spaceRegex.split(prev);
-
-        // If we can't find two words, or we found an empty word, return null.
-        if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
-
-        // If ends in a separator, return null
-        char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
-        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
-        return w[w.length - 2];
-    }
-
-    public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
-        if (null == connection) return null;
-        final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
-        return getThisWord(prev, sentenceSeperators);
-    }
-
-    // Get the word immediately before the cursor, even if there is whitespace between it and
-    // the cursor - but not if there is punctuation.
-    // Example :
-    // "abc def|" -> def
-    // "abc def |" -> def
-    // "abc def. |" -> null
-    // "abc def . |" -> null
-    public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
-        if (prev == null) return null;
-        String[] w = spaceRegex.split(prev);
-
-        // No word : return null
-        if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
-
-        // If ends in a separator, return null
-        char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
-        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
-        return w[w.length - 1];
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index c65404c..b93c17f 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -19,7 +19,9 @@
 import android.util.Log;
 
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
@@ -61,7 +63,7 @@
      * that filename.
      */
     private static final HashMap<String, DictionaryController> sSharedDictionaryControllers =
-            new HashMap<String, DictionaryController>();
+            CollectionUtils.newHashMap();
 
     /** The application context. */
     protected final Context mContext;
@@ -75,9 +77,6 @@
     /** The expandable fusion dictionary used to generate the binary dictionary. */
     private FusionDictionary mFusionDictionary;
 
-    /** The dictionary type id. */
-    public final int mDicTypeId;
-
     /**
      * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
      * dictionary instances with the same filename is supported, with access controlled by
@@ -91,6 +90,10 @@
     /** Controls access to the local binary dictionary for this instance. */
     private final DictionaryController mLocalDictionaryController = new DictionaryController();
 
+    private static final int BINARY_DICT_VERSION = 1;
+    private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
+            new FormatSpec.FormatOptions(BINARY_DICT_VERSION);
+
     /**
      * Abstract method for loading the unigrams and bigrams of a given dictionary in a background
      * thread.
@@ -123,11 +126,11 @@
      * @param context The application context of the parent.
      * @param filename The filename for this binary dictionary. Multiple dictionaries with the same
      *        filename is supported.
-     * @param dictType The type of this dictionary.
+     * @param dictType the dictionary type, as a human-readable string
      */
     public ExpandableBinaryDictionary(
-            final Context context, final String filename, final int dictType) {
-        mDicTypeId = dictType;
+            final Context context, final String filename, final String dictType) {
+        super(dictType);
         mFilename = filename;
         mContext = context;
         mBinaryDictionary = null;
@@ -161,9 +164,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));
     }
 
     /**
@@ -174,12 +177,12 @@
     // considering performance regression.
     protected void addWord(final String word, final String shortcutTarget, final int frequency) {
         if (shortcutTarget == null) {
-            mFusionDictionary.add(word, frequency, null);
+            mFusionDictionary.add(word, frequency, null, false /* isNotAWord */);
         } 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);
+            mFusionDictionary.add(word, frequency, shortcutTargets, false /* isNotAWord */);
         }
     }
 
@@ -194,46 +197,19 @@
     }
 
     @Override
-    public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
-            final WordCallback callback, final ProximityInfo proximityInfo) {
+    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo) {
         asyncReloadDictionaryIfRequired();
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    protected final void getWordsInner(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
-        // Ensure that there are no concurrent calls to getWords. If there are, do nothing and
-        // return.
         if (mLocalDictionaryController.tryLock()) {
             try {
                 if (mBinaryDictionary != null) {
-                    mBinaryDictionary.getWords(codes, prevWordForBigrams, callback, proximityInfo);
+                    return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo);
                 }
             } finally {
                 mLocalDictionaryController.unlock();
             }
         }
-    }
-
-    @Override
-    public void getBigrams(final WordComposer codes, final CharSequence previousWord,
-            final WordCallback callback) {
-        asyncReloadDictionaryIfRequired();
-        getBigramsInner(codes, previousWord, callback);
-    }
-
-    protected void getBigramsInner(final WordComposer codes, final CharSequence previousWord,
-            final WordCallback callback) {
-        if (mLocalDictionaryController.tryLock()) {
-            try {
-                if (mBinaryDictionary != null) {
-                    mBinaryDictionary.getBigrams(codes, previousWord, callback);
-                }
-            } finally {
-                mLocalDictionaryController.unlock();
-            }
-        }
+        return null;
     }
 
     @Override
@@ -306,7 +282,7 @@
         // Build the new binary dictionary
         final BinaryDictionary newBinaryDictionary =
                 new BinaryDictionary(mContext, filename, 0, length, true /* useFullEditDistance */,
-                        null);
+                        null, mDictType);
 
         if (mBinaryDictionary != null) {
             // Ensure all threads accessing the current dictionary have finished before swapping in
@@ -339,7 +315,7 @@
         FileOutputStream out = null;
         try {
             out = new FileOutputStream(tempFile);
-            BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, 1);
+            BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS);
             out.flush();
             out.close();
             tempFile.renameTo(file);
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index 34a92fd..8a38d1e 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -17,10 +17,11 @@
 package com.android.inputmethod.latin;
 
 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;
 import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
 
 import java.util.ArrayList;
@@ -37,7 +38,6 @@
 
     private Context mContext;
     private char[] mWordBuilder = new char[BinaryDictionary.MAX_WORD_LENGTH];
-    private int mDicTypeId;
     private int mMaxDepth;
     private int mInputLength;
 
@@ -151,11 +151,11 @@
 
     private int[][] mCodes;
 
-    public ExpandableDictionary(Context context, int dicTypeId) {
+    public ExpandableDictionary(final Context context, final String dictType) {
+        super(dictType);
         mContext = context;
         clearDictionary();
         mCodes = new int[BinaryDictionary.MAX_WORD_LENGTH][];
-        mDicTypeId = dicTypeId;
     }
 
     public void loadDictionary() {
@@ -230,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 {
@@ -247,27 +247,43 @@
     }
 
     @Override
-    public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
-            final WordCallback callback, final ProximityInfo proximityInfo) {
+    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo) {
+        if (reloadDictionaryIfRequired()) return null;
+        if (composer.size() > 1) {
+            if (composer.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
+                return null;
+            }
+            final ArrayList<SuggestedWordInfo> suggestions =
+                    getWordsInner(composer, prevWord, proximityInfo);
+            return suggestions;
+        } else {
+            if (TextUtils.isEmpty(prevWord)) return null;
+            final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+            runBigramReverseLookUp(prevWord, suggestions);
+            return suggestions;
+        }
+    }
+
+    // This reloads the dictionary if required, and returns whether it's currently updating its
+    // contents or not.
+    // @VisibleForTesting
+    boolean reloadDictionaryIfRequired() {
         synchronized (mUpdatingLock) {
             // If we need to update, start off a background task
             if (mRequiresReload) startDictionaryLoadingTaskLocked();
-            // Currently updating contacts, don't return any results.
-            if (mUpdatingDictionary) return;
+            return mUpdatingDictionary;
         }
-        if (codes.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
-            return;
-        }
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
     }
 
-    protected final void getWordsInner(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
+    protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
+            final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
+        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
         mInputLength = codes.size();
         if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
-        final int[] xCoordinates = codes.getXCoordinates();
-        final int[] yCoordinates = codes.getYCoordinates();
+        final InputPointers ips = codes.getInputPointers();
+        final int[] xCoordinates = ips.getXCoordinates();
+        final int[] yCoordinates = ips.getYCoordinates();
         // Cache the codes so that we don't have to lookup an array list
         for (int i = 0; i < mInputLength; i++) {
             // TODO: Calculate proximity info here.
@@ -275,16 +291,17 @@
                 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;
-        getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, callback);
+        getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, suggestions);
         for (int i = 0; i < mInputLength; i++) {
-            getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, callback);
+            getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, suggestions);
         }
+        return suggestions;
     }
 
     @Override
@@ -368,24 +385,27 @@
      * @param word the word to insert, as an array of code points
      * @param depth the depth of the node in the tree
      * @param finalFreq the frequency for this word
+     * @param suggestions the suggestion collection to add the suggestions to
      * @return whether there is still space for more words.
-     * @see Dictionary.WordCallback#addWord(char[], int, int, int, int, int)
      */
     private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth,
-            final int finalFreq, final WordCallback callback) {
+            final int finalFreq, final ArrayList<SuggestedWordInfo> suggestions) {
         if (finalFreq > 0 && !node.mShortcutOnly) {
-            if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, Dictionary.UNIGRAM)) {
-                return false;
-            }
+            // Use KIND_CORRECTION always. This dictionary does not really have a notion of
+            // COMPLETION against CORRECTION; we could artificially add one by looking at
+            // the respective size of the typed word and the suggestion if it matters sometime
+            // in the future.
+            suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq,
+                    SuggestedWordInfo.KIND_CORRECTION, mDictType));
+            if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false;
         }
         if (null != node.mShortcutTargets) {
             final int length = node.mShortcutTargets.size();
             for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) {
                 final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
-                if (!callback.addWord(shortcut, 0, shortcut.length, finalFreq, mDicTypeId,
-                        Dictionary.UNIGRAM)) {
-                    return false;
-                }
+                suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length),
+                        finalFreq, SuggestedWordInfo.KIND_SHORTCUT, mDictType));
+                if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
             }
         }
         return true;
@@ -408,12 +428,12 @@
      * case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
      * "wouldve", it could be matching "would've", so the depth will be one more than the
      * inputIndex
-     * @param callback the callback class for adding a word
+     * @param suggestions the list in which to add suggestions
      */
     // TODO: Share this routine with the native code for BinaryDictionary
     protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
             final int depth, final boolean completion, int snr, int inputIndex, int skipPos,
-            WordCallback callback) {
+            final ArrayList<SuggestedWordInfo> suggestions) {
         final int count = roots.mLength;
         final int codeSize = mInputLength;
         // Optimization: Prune out words that are too long compared to how much was typed.
@@ -443,14 +463,14 @@
                     } else {
                         finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength);
                     }
-                    if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, callback)) {
+                    if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, suggestions)) {
                         // No space left in the queue, bail out
                         return;
                     }
                 }
                 if (children != null) {
                     getWordsRec(children, codes, word, depth + 1, true, snr, inputIndex,
-                            skipPos, callback);
+                            skipPos, suggestions);
                 }
             } else if ((c == Keyboard.CODE_SINGLE_QUOTE
                     && currentChars[0] != Keyboard.CODE_SINGLE_QUOTE) || depth == skipPos) {
@@ -458,7 +478,7 @@
                 word[depth] = c;
                 if (children != null) {
                     getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
-                            skipPos, callback);
+                            skipPos, suggestions);
                 }
             } else {
                 // Don't use alternatives if we're looking for missing characters
@@ -466,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) {
@@ -483,7 +503,7 @@
                                             snr * addedAttenuation, mInputLength);
                                 }
                                 if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq,
-                                        callback)) {
+                                        suggestions)) {
                                     // No space left in the queue, bail out
                                     return;
                                 }
@@ -491,12 +511,12 @@
                             if (children != null) {
                                 getWordsRec(children, codes, word, depth + 1,
                                         true, snr * addedAttenuation, inputIndex + 1,
-                                        skipPos, callback);
+                                        skipPos, suggestions);
                             }
                         } else if (children != null) {
                             getWordsRec(children, codes, word, depth + 1,
                                     false, snr * addedAttenuation, inputIndex + 1,
-                                    skipPos, callback);
+                                    skipPos, suggestions);
                         }
                     }
                 }
@@ -514,8 +534,10 @@
 
     /**
      * Adds bigrams to the in-memory trie structure that is being used to retrieve any word
+     * @param word1 the first word of this bigram
+     * @param word2 the second word of this bigram
      * @param frequency frequency for this bigram
-     * @param addFrequency if true, it adds to current frequency, else it overwrites the old value
+     * @param fcp an instance of ForgettingCurveParams to use for decay policy
      * @return returns the final bigram frequency
      */
     private int setBigramAndGetFrequency(
@@ -528,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) {
@@ -580,32 +602,14 @@
         return searchWord(childNode.mChildren, word, depth + 1, childNode);
     }
 
-    // @VisibleForTesting
-    boolean reloadDictionaryIfRequired() {
-        synchronized (mUpdatingLock) {
-            // If we need to update, start off a background task
-            if (mRequiresReload) startDictionaryLoadingTaskLocked();
-            // Currently updating contacts, don't return any results.
-            return mUpdatingDictionary;
-        }
-    }
-
     private void runBigramReverseLookUp(final CharSequence previousWord,
-            final WordCallback callback) {
+            final ArrayList<SuggestedWordInfo> suggestions) {
         // Search for the lowercase version of the word only, because that's where bigrams
         // store their sons.
         Node prevWord = searchNode(mRoots, previousWord.toString().toLowerCase(), 0,
                 previousWord.length());
         if (prevWord != null && prevWord.mNGrams != null) {
-            reverseLookUp(prevWord.mNGrams, callback);
-        }
-    }
-
-    @Override
-    public void getBigrams(final WordComposer codes, final CharSequence previousWord,
-            final WordCallback callback) {
-        if (!reloadDictionaryIfRequired()) {
-            runBigramReverseLookUp(previousWord, callback);
+            reverseLookUp(prevWord.mNGrams, suggestions);
         }
     }
 
@@ -633,11 +637,12 @@
 
     /**
      * reverseLookUp retrieves the full word given a list of terminal nodes and adds those words
-     * through callback.
+     * to the suggestions list passed as an argument.
      * @param terminalNodes list of terminal nodes we want to add
+     * @param suggestions the suggestion collection to add the word to
      */
     private void reverseLookUp(LinkedList<NextWord> terminalNodes,
-            final WordCallback callback) {
+            final ArrayList<SuggestedWordInfo> suggestions) {
         Node node;
         int freq;
         for (NextWord nextWord : terminalNodes) {
@@ -648,11 +653,15 @@
                 --index;
                 mLookedUpString[index] = node.mCode;
                 node = node.mParent;
-            } while (node != null);
+            } while (node != null && index > 0);
 
-            if (freq >= 0) {
-                callback.addWord(mLookedUpString, index, BinaryDictionary.MAX_WORD_LENGTH - index,
-                        freq, mDicTypeId, Dictionary.BIGRAM);
+            // If node is null, we have a word longer than MAX_WORD_LENGTH in the dictionary.
+            // It's a little unclear how this can happen, but just in case it does it's safer
+            // to ignore the word in this case.
+            if (freq >= 0 && node == null) {
+                suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index,
+                        BinaryDictionary.MAX_WORD_LENGTH - index),
+                        freq, SuggestedWordInfo.KIND_CORRECTION, mDictType));
             }
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/ImfUtils.java b/java/src/com/android/inputmethod/latin/ImfUtils.java
index b882a48..2674e45 100644
--- a/java/src/com/android/inputmethod/latin/ImfUtils.java
+++ b/java/src/com/android/inputmethod/latin/ImfUtils.java
@@ -29,7 +29,7 @@
 /**
  * Utility class for Input Method Framework
  */
-public class ImfUtils {
+public final class ImfUtils {
     private ImfUtils() {
         // This utility class is not publicly instantiable.
     }
@@ -90,6 +90,13 @@
         return false;
     }
 
+    public static InputMethodSubtype getCurrentInputMethodSubtype(Context context,
+            InputMethodSubtype defaultSubtype) {
+        final InputMethodManager imm = getInputMethodManager(context);
+        final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
+        return (currentSubtype != null) ? currentSubtype : defaultSubtype;
+    }
+
     public static boolean hasMultipleEnabledIMEsOrSubtypes(Context context,
             final boolean shouldIncludeAuxiliarySubtypes) {
         final InputMethodManager imm = getInputMethodManager(context);
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index 229ae2f..7bcda9b 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -29,11 +29,12 @@
     final public boolean mInputTypeNoAutoCorrect;
     final public boolean mIsSettingsSuggestionStripOn;
     final public boolean mApplicationSpecifiedCompletionOn;
-    final public int mEditorAction;
+    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
@@ -64,7 +65,7 @@
             final boolean flagAutoComplete =
                     0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
 
-            // Make sure that passwords are not displayed in {@link SuggestionsView}.
+            // Make sure that passwords are not displayed in {@link SuggestionStripView}.
             if (InputTypeUtils.isPasswordInputType(inputType)
                     || InputTypeUtils.isVisiblePasswordInputType(inputType)
                     || InputTypeUtils.isEmailVariation(variation)
@@ -92,8 +93,10 @@
 
             mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode;
         }
-        mEditorAction = (editorInfo == null) ? EditorInfo.IME_ACTION_UNSPECIFIED
-                : editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+    }
+
+    public boolean isSameInputType(final EditorInfo editorInfo) {
+        return editorInfo.inputType == mInputType;
     }
 
     @SuppressWarnings("unused")
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
new file mode 100644
index 0000000..ff2feb5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/InputPointers.java
@@ -0,0 +1,133 @@
+/*
+ * 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;
+
+// TODO: This class is not thread-safe.
+public class InputPointers {
+    private final int mDefaultCapacity;
+    private final ResizableIntArray mXCoordinates;
+    private final ResizableIntArray mYCoordinates;
+    private final ResizableIntArray mPointerIds;
+    private final ResizableIntArray mTimes;
+
+    public InputPointers(int defaultCapacity) {
+        mDefaultCapacity = defaultCapacity;
+        mXCoordinates = new ResizableIntArray(defaultCapacity);
+        mYCoordinates = new ResizableIntArray(defaultCapacity);
+        mPointerIds = new ResizableIntArray(defaultCapacity);
+        mTimes = new ResizableIntArray(defaultCapacity);
+    }
+
+    public void addPointer(int index, int x, int y, int pointerId, int time) {
+        mXCoordinates.add(index, x);
+        mYCoordinates.add(index, y);
+        mPointerIds.add(index, pointerId);
+        mTimes.add(index, time);
+    }
+
+    public void addPointer(int x, int y, int pointerId, int time) {
+        mXCoordinates.add(x);
+        mYCoordinates.add(y);
+        mPointerIds.add(pointerId);
+        mTimes.add(time);
+    }
+
+    public void set(InputPointers ip) {
+        mXCoordinates.set(ip.mXCoordinates);
+        mYCoordinates.set(ip.mYCoordinates);
+        mPointerIds.set(ip.mPointerIds);
+        mTimes.set(ip.mTimes);
+    }
+
+    public void copy(InputPointers ip) {
+        mXCoordinates.copy(ip.mXCoordinates);
+        mYCoordinates.copy(ip.mYCoordinates);
+        mPointerIds.copy(ip.mPointerIds);
+        mTimes.copy(ip.mTimes);
+    }
+
+    /**
+     * Append the pointers in the specified {@link InputPointers} to the end of this.
+     * @param src the source {@link InputPointers} to read the data from.
+     * @param startPos the starting index of the pointers in {@code src}.
+     * @param length the number of pointers to be appended.
+     */
+    public void append(InputPointers src, int startPos, int length) {
+        if (length == 0) {
+            return;
+        }
+        mXCoordinates.append(src.mXCoordinates, startPos, length);
+        mYCoordinates.append(src.mYCoordinates, startPos, length);
+        mPointerIds.append(src.mPointerIds, startPos, length);
+        mTimes.append(src.mTimes, startPos, length);
+    }
+
+    /**
+     * Append the times, x-coordinates and y-coordinates in the specified {@link ResizableIntArray}
+     * to the end of this.
+     * @param pointerId the pointer id of the source.
+     * @param times the source {@link ResizableIntArray} to read the event times from.
+     * @param xCoordinates the source {@link ResizableIntArray} to read the x-coordinates from.
+     * @param yCoordinates the source {@link ResizableIntArray} to read the y-coordinates from.
+     * @param startPos the starting index of the data in {@code times} and etc.
+     * @param length the number of data to be appended.
+     */
+    public void append(int pointerId, ResizableIntArray times, ResizableIntArray xCoordinates,
+            ResizableIntArray yCoordinates, int startPos, int length) {
+        if (length == 0) {
+            return;
+        }
+        mXCoordinates.append(xCoordinates, startPos, length);
+        mYCoordinates.append(yCoordinates, startPos, length);
+        mPointerIds.fill(pointerId, mPointerIds.getLength(), length);
+        mTimes.append(times, startPos, length);
+    }
+
+    public void reset() {
+        final int defaultCapacity = mDefaultCapacity;
+        mXCoordinates.reset(defaultCapacity);
+        mYCoordinates.reset(defaultCapacity);
+        mPointerIds.reset(defaultCapacity);
+        mTimes.reset(defaultCapacity);
+    }
+
+    public int getPointerSize() {
+        return mXCoordinates.getLength();
+    }
+
+    public int[] getXCoordinates() {
+        return mXCoordinates.getPrimitiveArray();
+    }
+
+    public int[] getYCoordinates() {
+        return mYCoordinates.getPrimitiveArray();
+    }
+
+    public int[] getPointerIds() {
+        return mPointerIds.getPrimitiveArray();
+    }
+
+    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/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
index 40c3b76..500866a 100644
--- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
@@ -18,7 +18,7 @@
 
 import android.text.InputType;
 
-public class InputTypeUtils implements InputType {
+public final class InputTypeUtils implements InputType {
     private static final int WEB_TEXT_PASSWORD_INPUT_TYPE =
             TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_PASSWORD;
     private static final int WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE =
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
index 0dcb811..c15f453 100644
--- a/java/src/com/android/inputmethod/latin/InputView.java
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -24,7 +24,7 @@
 import android.widget.LinearLayout;
 
 public class InputView extends LinearLayout {
-    private View mSuggestionsContainer;
+    private View mSuggestionStripContainer;
     private View mKeyboardView;
     private int mKeyboardTopPadding;
 
@@ -43,13 +43,13 @@
 
     @Override
     protected void onFinishInflate() {
-        mSuggestionsContainer = findViewById(R.id.suggestions_container);
+        mSuggestionStripContainer = findViewById(R.id.suggestions_container);
         mKeyboardView = findViewById(R.id.keyboard_view);
     }
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent me) {
-        if (mSuggestionsContainer.getVisibility() == VISIBLE
+        if (mSuggestionStripContainer.getVisibility() == VISIBLE
                 && mKeyboardView.getVisibility() == VISIBLE
                 && forwardTouchEvent(me)) {
             return true;
@@ -57,7 +57,8 @@
         return super.dispatchTouchEvent(me);
     }
 
-    // The touch events that hit the top padding of keyboard should be forwarded to SuggestionsView.
+    // The touch events that hit the top padding of keyboard should be forwarded to
+    // {@link SuggestionStripView}.
     private boolean forwardTouchEvent(MotionEvent me) {
         final Rect rect = mInputViewRect;
         this.getGlobalVisibleRect(rect);
@@ -96,7 +97,7 @@
         }
 
         final Rect receivingRect = mEventReceivingRect;
-        mSuggestionsContainer.getGlobalVisibleRect(receivingRect);
+        mSuggestionStripContainer.getGlobalVisibleRect(receivingRect);
         final int translatedX = x - receivingRect.left;
         final int translatedY;
         if (y < forwardingLimitY) {
@@ -106,7 +107,7 @@
             translatedY = y - receivingRect.top;
         }
         me.setLocation(translatedX, translatedY);
-        mSuggestionsContainer.dispatchTouchEvent(me);
+        mSuggestionStripContainer.dispatchTouchEvent(me);
         return true;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/JniUtils.java
index 4808b86..f930599 100644
--- a/java/src/com/android/inputmethod/latin/JniUtils.java
+++ b/java/src/com/android/inputmethod/latin/JniUtils.java
@@ -20,7 +20,7 @@
 
 import com.android.inputmethod.latin.define.JniLibName;
 
-public class JniUtils {
+public final class JniUtils {
     private static final String TAG = JniUtils.class.getSimpleName();
 
     private JniUtils() {
@@ -31,11 +31,7 @@
         try {
             System.loadLibrary(JniLibName.JNI_LIB_NAME);
         } catch (UnsatisfiedLinkError ule) {
-            Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME);
-            if (LatinImeLogger.sDBG) {
-                throw new RuntimeException(
-                        "Could not load native library " + JniLibName.JNI_LIB_NAME);
-            }
+            Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME, ule);
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
index 4e1f5fe..dd73a97 100644
--- a/java/src/com/android/inputmethod/latin/LastComposedWord.java
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -38,32 +38,32 @@
     // an auto-correction.
     public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3;
 
-    public static final int NOT_A_SEPARATOR = -1;
+    public static final String NOT_A_SEPARATOR = "";
 
     public final int[] mPrimaryKeyCodes;
-    public final int[] mXCoordinates;
-    public final int[] mYCoordinates;
     public final String mTypedWord;
     public final String mCommittedWord;
-    public final int mSeparatorCode;
+    public final String mSeparatorString;
     public final CharSequence mPrevWord;
+    public final InputPointers mInputPointers = new InputPointers(BinaryDictionary.MAX_WORD_LENGTH);
 
     private boolean mActive;
 
     public static final LastComposedWord NOT_A_COMPOSED_WORD =
-            new LastComposedWord(null, null, null, "", "", NOT_A_SEPARATOR, null);
+            new LastComposedWord(null, null, "", "", NOT_A_SEPARATOR, null);
 
     // Warning: this is using the passed objects as is and fully expects them to be
     // immutable. Do not fiddle with their contents after you passed them to this constructor.
-    public LastComposedWord(final int[] primaryKeyCodes, final int[] xCoordinates,
-            final int[] yCoordinates, final String typedWord, final String committedWord,
-            final int separatorCode, final CharSequence prevWord) {
+    public LastComposedWord(final int[] primaryKeyCodes, final InputPointers inputPointers,
+            final String typedWord, final String committedWord,
+            final String separatorString, final CharSequence prevWord) {
         mPrimaryKeyCodes = primaryKeyCodes;
-        mXCoordinates = xCoordinates;
-        mYCoordinates = yCoordinates;
+        if (inputPointers != null) {
+            mInputPointers.copy(inputPointers);
+        }
         mTypedWord = typedWord;
         mCommittedWord = committedWord;
-        mSeparatorCode = separatorCode;
+        mSeparatorString = separatorString;
         mActive = true;
         mPrevWord = prevWord;
     }
@@ -73,14 +73,14 @@
     }
 
     public boolean canRevertCommit() {
-        return mActive && !TextUtils.isEmpty(mCommittedWord);
+        return mActive && !TextUtils.isEmpty(mCommittedWord) && !didCommitTypedWord();
     }
 
-    public boolean didCommitTypedWord() {
+    private boolean didCommitTypedWord() {
         return TextUtils.equals(mTypedWord, mCommittedWord);
     }
 
-    public static int getSeparatorLength(final int separatorCode) {
-        return NOT_A_SEPARATOR == separatorCode ? 0 : 1;
+    public static int getSeparatorLength(final String separatorString) {
+        return StringUtils.codePointCount(separatorString);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 97e898a..9252b09 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -20,6 +20,7 @@
 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
 
+import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -35,10 +36,11 @@
 import android.media.AudioManager;
 import android.net.ConnectivityManager;
 import android.os.Debug;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.SystemClock;
-import android.preference.PreferenceActivity;
 import android.preference.PreferenceManager;
 import android.text.InputType;
 import android.text.TextUtils;
@@ -48,31 +50,32 @@
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
-import android.view.ViewParent;
 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.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.accessibility.AccessibilityUtils;
 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
 import com.android.inputmethod.compat.CompatUtils;
 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
 import com.android.inputmethod.compat.SuggestionSpanUtils;
+import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardActionListener;
 import com.android.inputmethod.keyboard.KeyboardId;
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.KeyboardView;
-import com.android.inputmethod.keyboard.LatinKeyboardView;
+import com.android.inputmethod.keyboard.MainKeyboardView;
 import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+import com.android.inputmethod.latin.Utils.Stats;
 import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.latin.suggestions.SuggestionsView;
+import com.android.inputmethod.latin.suggestions.SuggestionStripView;
+import com.android.inputmethod.research.ResearchLogger;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -83,7 +86,8 @@
  * Input method implementation for Qwerty'ish keyboard.
  */
 public class LatinIME extends InputMethodService implements KeyboardActionListener,
-        SuggestionsView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener {
+        SuggestionStripView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener,
+        Suggest.SuggestInitializationListener {
     private static final String TAG = LatinIME.class.getSimpleName();
     private static final boolean TRACE = false;
     private static boolean DEBUG;
@@ -103,27 +107,6 @@
      */
     private static final String SCHEME_PACKAGE = "package";
 
-    /** Whether to use the binary version of the contacts dictionary */
-    public static final boolean USE_BINARY_CONTACTS_DICTIONARY = true;
-
-    /** Whether to use the binary version of the user dictionary */
-    public static final boolean USE_BINARY_USER_DICTIONARY = true;
-
-    // TODO: migrate this to SettingsValues
-    private int mSuggestionVisibility;
-    private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
-            = R.string.prefs_suggestion_visibility_show_value;
-    private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
-            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
-    private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
-            = R.string.prefs_suggestion_visibility_hide_value;
-
-    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
-        SUGGESTION_VISIBILILTY_SHOW_VALUE,
-        SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
-        SUGGESTION_VISIBILILTY_HIDE_VALUE
-    };
-
     private static final int SPACE_STATE_NONE = 0;
     // Double space: the state where the user pressed space twice quickly, which LatinIME
     // resolved as period-space. Undoing this converts the period to a space.
@@ -143,13 +126,12 @@
     // Current space state of the input method. This can be any of the above constants.
     private int mSpaceState;
 
-    private SettingsValues mSettingsValues;
-    private InputAttributes mInputAttributes;
+    private SettingsValues mCurrentSettings;
 
     private View mExtractArea;
     private View mKeyPreviewBackingView;
     private View mSuggestionsContainer;
-    private SuggestionsView mSuggestionsView;
+    private SuggestionStripView mSuggestionStripView;
     /* package for tests */ Suggest mSuggest;
     private CompletionInfo[] mApplicationSpecifiedCompletions;
     private ApplicationInfo mTargetApplicationInfo;
@@ -162,15 +144,13 @@
     private boolean mShouldSwitchToLastSubtype = true;
 
     private boolean mIsMainDictionaryAvailable;
-    // TODO: revert this back to the concrete class after transition.
-    private Dictionary mUserDictionary;
+    private UserBinaryDictionary mUserDictionary;
     private UserHistoryDictionary mUserHistoryDictionary;
     private boolean mIsUserDictionaryAvailable;
 
     private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
-    private WordComposer mWordComposer = new WordComposer();
-
-    private int mCorrectionMode;
+    private final WordComposer mWordComposer = new WordComposer();
+    private RichInputConnection mConnection = new RichInputConnection(this);
 
     // Keep track of the last selection range to decide if we need to show word alternatives
     private static final int NOT_A_CURSOR_POSITION = -1;
@@ -199,20 +179,24 @@
 
     private AlertDialog mOptionsDialog;
 
+    private final boolean mIsHardwareAcceleratedDrawingEnabled;
+
     public final UIHandler mHandler = new UIHandler(this);
 
     public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
-        private static final int MSG_UPDATE_SHIFT_STATE = 1;
-        private static final int MSG_SPACE_TYPED = 4;
-        private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
-        private static final int MSG_PENDING_IMS_CALLBACK = 6;
-        private static final int MSG_UPDATE_SUGGESTIONS = 7;
+        private static final int MSG_UPDATE_SHIFT_STATE = 0;
+        private static final int MSG_PENDING_IMS_CALLBACK = 1;
+        private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
+        private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
+
+        private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
 
         private int mDelayUpdateSuggestions;
         private int mDelayUpdateShiftState;
         private long mDoubleSpacesTurnIntoPeriodTimeout;
+        private long mDoubleSpaceTimerStart;
 
-        public UIHandler(LatinIME outerInstance) {
+        public UIHandler(final LatinIME outerInstance) {
             super(outerInstance);
         }
 
@@ -227,33 +211,33 @@
         }
 
         @Override
-        public void handleMessage(Message msg) {
+        public void handleMessage(final Message msg) {
             final LatinIME latinIme = getOuterInstance();
             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
             switch (msg.what) {
-            case MSG_UPDATE_SUGGESTIONS:
-                latinIme.updateSuggestions();
+            case MSG_UPDATE_SUGGESTION_STRIP:
+                latinIme.updateSuggestionStrip();
                 break;
             case MSG_UPDATE_SHIFT_STATE:
                 switcher.updateShiftState();
                 break;
-            case MSG_SET_BIGRAM_PREDICTIONS:
-                latinIme.updateBigramPredictions();
+            case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
+                latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj,
+                        msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
                 break;
             }
         }
 
-        public void postUpdateSuggestions() {
-            removeMessages(MSG_UPDATE_SUGGESTIONS);
-            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
+        public void postUpdateSuggestionStrip() {
+            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
         }
 
-        public void cancelUpdateSuggestions() {
-            removeMessages(MSG_UPDATE_SUGGESTIONS);
+        public void cancelUpdateSuggestionStrip() {
+            removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
         }
 
         public boolean hasPendingUpdateSuggestions() {
-            return hasMessages(MSG_UPDATE_SUGGESTIONS);
+            return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
         }
 
         public void postUpdateShiftState() {
@@ -265,26 +249,26 @@
             removeMessages(MSG_UPDATE_SHIFT_STATE);
         }
 
-        public void postUpdateBigramPredictions() {
-            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
-            sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
-        }
-
-        public void cancelUpdateBigramPredictions() {
-            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
+        public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
+                final boolean dismissGestureFloatingPreviewText) {
+            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
+            final int arg1 = dismissGestureFloatingPreviewText
+                    ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0;
+            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords)
+                    .sendToTarget();
         }
 
         public void startDoubleSpacesTimer() {
-            removeMessages(MSG_SPACE_TYPED);
-            sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout);
+            mDoubleSpaceTimerStart = SystemClock.uptimeMillis();
         }
 
         public void cancelDoubleSpacesTimer() {
-            removeMessages(MSG_SPACE_TYPED);
+            mDoubleSpaceTimerStart = 0;
         }
 
         public boolean isAcceptingDoubleSpaces() {
-            return hasMessages(MSG_SPACE_TYPED);
+            return SystemClock.uptimeMillis() - mDoubleSpaceTimerStart
+                    < mDoubleSpacesTurnIntoPeriodTimeout;
         }
 
         // Working variables for the following methods.
@@ -311,7 +295,7 @@
             mHasPendingStartInput = false;
         }
 
-        private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo,
+        private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo,
                 boolean restarting) {
             if (mHasPendingFinishInputView)
                 latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
@@ -322,7 +306,7 @@
             resetPendingImsCallback();
         }
 
-        public void onStartInput(EditorInfo editorInfo, boolean restarting) {
+        public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
                 // Typically this is the second onStartInput after orientation changed.
                 mHasPendingStartInput = true;
@@ -338,7 +322,7 @@
             }
         }
 
-        public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
+        public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
             if (hasMessages(MSG_PENDING_IMS_CALLBACK)
                     && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
                 // Typically this is the second onStartInputView after orientation changed.
@@ -358,7 +342,7 @@
             }
         }
 
-        public void onFinishInputView(boolean finishingInput) {
+        public void onFinishInputView(final boolean finishingInput) {
             if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
                 // Typically this is the first onFinishInputView after orientation changed.
                 mHasPendingFinishInputView = true;
@@ -385,6 +369,9 @@
         super();
         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
+        mIsHardwareAcceleratedDrawingEnabled =
+                InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
+        Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
     }
 
     @Override
@@ -393,7 +380,7 @@
         mPrefs = prefs;
         LatinImeLogger.init(this, prefs);
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.init(this, prefs);
+            ResearchLogger.getInstance().init(this, prefs);
         }
         InputMethodManagerCompatWrapper.init(this);
         SubtypeSwitcher.init(this);
@@ -411,22 +398,9 @@
 
         loadSettings();
 
-        ImfUtils.setAdditionalInputMethodSubtypes(this, mSettingsValues.getAdditionalSubtypes());
+        ImfUtils.setAdditionalInputMethodSubtypes(this, mCurrentSettings.getAdditionalSubtypes());
 
-        // TODO: remove the following when it's not needed by updateCorrectionMode() any more
-        mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
-        updateCorrectionMode();
-
-        Utils.GCUtils.getInstance().reset();
-        boolean tryGC = true;
-        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;
 
@@ -450,46 +424,58 @@
     }
 
     // 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);
+        final InputAttributes inputAttributes =
+                new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
         final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
             @Override
             protected SettingsValues job(Resources res) {
-                return new SettingsValues(mPrefs, LatinIME.this);
+                return new SettingsValues(mPrefs, inputAttributes, LatinIME.this);
             }
         };
-        mSettingsValues = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
-        mFeedbackManager = new AudioAndHapticFeedbackManager(this, mSettingsValues);
+        mCurrentSettings = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
+        mFeedbackManager = new AudioAndHapticFeedbackManager(this, mCurrentSettings);
         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
     }
 
+    // Note that this method is called from a non-UI thread.
+    @Override
+    public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
+        mIsMainDictionaryAvailable = isMainDictionaryAvailable;
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView != null) {
+            mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
+        }
+    }
+
     private void initSuggest() {
         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
         final String localeStr = subtypeLocale.toString();
 
-        final Dictionary oldContactsDictionary;
+        final ContactsBinaryDictionary oldContactsDictionary;
         if (mSuggest != null) {
             oldContactsDictionary = mSuggest.getContactsDictionary();
             mSuggest.close();
         } else {
             oldContactsDictionary = null;
         }
-        mSuggest = new Suggest(this, subtypeLocale);
-        if (mSettingsValues.mAutoCorrectEnabled) {
-            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+        mSuggest = new Suggest(this /* Context */, subtypeLocale,
+                this /* SuggestInitializationListener */);
+        if (mCurrentSettings.mCorrectionEnabled) {
+            mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
         }
 
         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
-
-        if (USE_BINARY_USER_DICTIONARY) {
-            mUserDictionary = new UserBinaryDictionary(this, localeStr);
-            mIsUserDictionaryAvailable = ((UserBinaryDictionary)mUserDictionary).isEnabled();
-        } else {
-            mUserDictionary = new UserDictionary(this, localeStr);
-            mIsUserDictionaryAvailable = ((UserDictionary)mUserDictionary).isEnabled();
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().initSuggest(mSuggest);
         }
+
+        mUserDictionary = new UserBinaryDictionary(this, localeStr);
+        mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
         mSuggest.setUserDictionary(mUserDictionary);
 
         resetContactsDictionary(oldContactsDictionary);
@@ -497,44 +483,43 @@
         // 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);
-        mUserHistoryDictionary = UserHistoryDictionary.getInstance(
-                this, localeStr, Suggest.DIC_USER_HISTORY, mPrefs);
+        mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, mPrefs);
         mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
     }
 
     /**
      * Resets the contacts dictionary in mSuggest according to the user settings.
      *
-     * This method takes an optional contacts dictionary to use. Since the contacts dictionary
-     * does not depend on the locale, it can be reused across different instances of Suggest.
-     * The dictionary will also be opened or closed as necessary depending on the settings.
+     * This method takes an optional contacts dictionary to use when the locale hasn't changed
+     * since the contacts dictionary can be opened or closed as necessary depending on the settings.
      *
      * @param oldContactsDictionary an optional dictionary to use, or null
      */
-    private void resetContactsDictionary(final Dictionary oldContactsDictionary) {
-        final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
+    private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
+        final boolean shouldSetDictionary = (null != mSuggest && mCurrentSettings.mUseContactsDict);
 
-        final Dictionary dictionaryToUse;
+        final ContactsBinaryDictionary dictionaryToUse;
         if (!shouldSetDictionary) {
             // Make sure the dictionary is closed. If it is already closed, this is a no-op,
             // so it's safe to call it anyways.
             if (null != oldContactsDictionary) oldContactsDictionary.close();
             dictionaryToUse = null;
-        } else if (null != oldContactsDictionary) {
-            // Make sure the old contacts dictionary is opened. If it is already open, this is a
-            // no-op, so it's safe to call it anyways.
-            if (USE_BINARY_CONTACTS_DICTIONARY) {
-                ((ContactsBinaryDictionary)oldContactsDictionary).reopen(this);
-            } else {
-                ((ContactsDictionary)oldContactsDictionary).reopen(this);
-            }
-            dictionaryToUse = oldContactsDictionary;
         } else {
-            if (USE_BINARY_CONTACTS_DICTIONARY) {
-                dictionaryToUse = new ContactsBinaryDictionary(this, Suggest.DIC_CONTACTS,
-                        mSubtypeSwitcher.getCurrentSubtypeLocale());
+            final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+            if (null != oldContactsDictionary) {
+                if (!oldContactsDictionary.mLocale.equals(locale)) {
+                    // If the locale has changed then recreate the contacts dictionary. This
+                    // allows locale dependent rules for handling bigram name predictions.
+                    oldContactsDictionary.close();
+                    dictionaryToUse = new ContactsBinaryDictionary(this, locale);
+                } else {
+                    // Make sure the old contacts dictionary is opened. If it is already open,
+                    // this is a no-op, so it's safe to call it anyways.
+                    oldContactsDictionary.reopen(this);
+                    dictionaryToUse = oldContactsDictionary;
+                }
             } else {
-                dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
+                dictionaryToUse = new ContactsBinaryDictionary(this, locale);
             }
         }
 
@@ -545,7 +530,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);
     }
 
@@ -563,59 +548,64 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration conf) {
-        mSubtypeSwitcher.onConfigurationChanged(conf);
+    public void onConfigurationChanged(final Configuration 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;
             mHandler.startOrientationChanging();
-            final InputConnection ic = getCurrentInputConnection();
-            commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
-            if (ic != null) ic.finishComposingText(); // For voice input
-            if (isShowingOptionDialog())
+            mConnection.beginBatchEdit();
+            commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+            mConnection.finishComposingText();
+            mConnection.endBatchEdit();
+            if (isShowingOptionDialog()) {
                 mOptionsDialog.dismiss();
+            }
         }
         super.onConfigurationChanged(conf);
     }
 
     @Override
     public View onCreateInputView() {
-        return mKeyboardSwitcher.onCreateInputView();
+        return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
     }
 
     @Override
-    public void setInputView(View view) {
+    public void setInputView(final View view) {
         super.setInputView(view);
         mExtractArea = getWindow().getWindow().getDecorView()
                 .findViewById(android.R.id.extractArea);
         mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
         mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
-        mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
-        if (mSuggestionsView != null)
-            mSuggestionsView.setListener(this, view);
+        mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
+        if (mSuggestionStripView != null)
+            mSuggestionStripView.setListener(this, view);
         if (LatinImeLogger.sVISUALDEBUG) {
             mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
         }
     }
 
     @Override
-    public void setCandidatesView(View view) {
+    public void setCandidatesView(final View view) {
         // To ensure that CandidatesView will never be set.
         return;
     }
 
     @Override
-    public void onStartInput(EditorInfo editorInfo, boolean restarting) {
+    public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
         mHandler.onStartInput(editorInfo, restarting);
     }
 
     @Override
-    public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
+    public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
         mHandler.onStartInputView(editorInfo, restarting);
     }
 
     @Override
-    public void onFinishInputView(boolean finishingInput) {
+    public void onFinishInputView(final boolean finishingInput) {
         mHandler.onFinishInputView(finishingInput);
     }
 
@@ -625,21 +615,22 @@
     }
 
     @Override
-    public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
+    public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
         // 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) {
+    private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
         super.onStartInput(editorInfo, restarting);
     }
 
     @SuppressWarnings("deprecation")
-    private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
+    private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
         super.onStartInputView(editorInfo, restarting);
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
-        LatinKeyboardView inputView = switcher.getKeyboardView();
+        final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
 
         if (editorInfo == null) {
             Log.e(TAG, "Null EditorInfo in onStartInputView()");
@@ -682,93 +673,141 @@
 
         LatinImeLogger.onStartInputView(editorInfo);
         // In landscape mode, this method gets called without the input view being created.
-        if (inputView == null) {
+        if (mainKeyboardView == null) {
             return;
         }
 
         // 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);
         }
 
-        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.
         // Note: This call should be done by InputMethodService?
         updateFullscreenMode();
-        mLastSelectionStart = editorInfo.initialSelStart;
-        mLastSelectionEnd = editorInfo.initialSelEnd;
-        mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
         mApplicationSpecifiedCompletions = null;
 
-        inputView.closing();
-        mEnteredText = null;
-        resetComposingState(true /* alsoResetLastComposedWord */);
-        mDeleteCount = 0;
-        mSpaceState = SPACE_STATE_NONE;
+        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
+            // or something.
+            mEnteredText = null;
+            resetComposingState(true /* alsoResetLastComposedWord */);
+            mDeleteCount = 0;
+            mSpaceState = SPACE_STATE_NONE;
 
-        loadSettings();
-        updateCorrectionMode();
-        updateSuggestionVisibility(mResources);
-
-        if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
-            mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+            if (mSuggestionStripView != null) {
+                mSuggestionStripView.clear();
+            }
         }
 
-        switcher.loadKeyboard(editorInfo, mSettingsValues);
+        mConnection.resetCachesUponCursorMove(mLastSelectionStart);
 
-        if (mSuggestionsView != null)
-            mSuggestionsView.clear();
+        if (isDifferentTextField) {
+            mainKeyboardView.closing();
+            loadSettings();
+
+            if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) {
+                mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
+            }
+
+            switcher.loadKeyboard(editorInfo, mCurrentSettings);
+        }
         setSuggestionStripShownInternal(
                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
-        // Delay updating suggestions because keyboard input view may not be shown at this point.
-        mHandler.postUpdateSuggestions();
+
+        mLastSelectionStart = editorInfo.initialSelStart;
+        mLastSelectionEnd = editorInfo.initialSelEnd;
+        // If we come here something in the text state is very likely to have changed.
+        // We should update the shift state regardless of whether we are restarting or not, because
+        // this is not perceived as a layout change that may be disruptive like we may have with
+        // switcher.loadKeyboard; in apps like Talk, we come here when the text is sent and the
+        // field gets emptied and we need to re-evaluate the shift state, but not the whole layout
+        // which would be disruptive.
+        // Space state must be updated before calling updateShiftState
+        mKeyboardSwitcher.updateShiftState();
+
+        mHandler.cancelUpdateSuggestionStrip();
         mHandler.cancelDoubleSpacesTimer();
 
-        inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
-                mSettingsValues.mKeyPreviewPopupDismissDelay);
-        inputView.setProximityCorrectionEnabled(true);
+        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");
     }
 
+    // Callback for the TargetApplicationGetter
+    @Override
     public void onTargetApplicationKnown(final ApplicationInfo info) {
         mTargetApplicationInfo = info;
     }
 
     @Override
     public void onWindowHidden() {
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd,
+                    getCurrentInputConnection());
+        }
         super.onWindowHidden();
-        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
-        if (inputView != null) inputView.closing();
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView != null) {
+            mainKeyboardView.closing();
+        }
     }
 
     private void onFinishInputInternal() {
         super.onFinishInput();
 
         LatinImeLogger.commit();
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().latinIME_onFinishInputInternal();
+        }
 
-        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
-        if (inputView != null) inputView.closing();
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView != null) {
+            mainKeyboardView.closing();
+        }
     }
 
-    private void onFinishInputViewInternal(boolean finishingInput) {
+    private void onFinishInputViewInternal(final boolean finishingInput) {
         super.onFinishInputView(finishingInput);
         mKeyboardSwitcher.onFinishInputView();
-        KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
-        if (inputView != null) inputView.cancelAllMessages();
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView != null) {
+            mainKeyboardView.cancelAllMessages();
+        }
         // Remove pending messages related to update suggestions
-        mHandler.cancelUpdateSuggestions();
+        mHandler.cancelUpdateSuggestionStrip();
     }
 
     @Override
-    public void onUpdateSelection(int oldSelStart, int oldSelEnd,
-            int newSelStart, int newSelEnd,
-            int composingSpanStart, int composingSpanEnd) {
+    public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
+            final int newSelStart, final int newSelEnd,
+            final int composingSpanStart, final int composingSpanEnd) {
         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
                 composingSpanStart, composingSpanEnd);
-
         if (DEBUG) {
             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
                     + ", ose=" + oldSelEnd
@@ -780,9 +819,16 @@
                     + ", ce=" + composingSpanEnd);
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
+            final boolean expectingUpdateSelectionFromLogger =
+                    ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
             ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
-                    composingSpanEnd);
+                    composingSpanEnd, mExpectingUpdateSelection,
+                    expectingUpdateSelectionFromLogger, mConnection);
+            if (expectingUpdateSelectionFromLogger) {
+                // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
+                return;
+            }
         }
 
         // TODO: refactor the following code to be less contrived.
@@ -799,7 +845,8 @@
         // we know for sure the cursor moved while we were composing and we should reset
         // the state.
         final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
-        if (!mExpectingUpdateSelection) {
+        if (!mExpectingUpdateSelection
+                && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
             // TAKE CARE: there is a race condition when we enter this test even when the user
             // did not explicitly move the cursor. This happens when typing fast, where two keys
             // turn this flag on in succession and both onUpdateSelection() calls arrive after
@@ -815,7 +862,7 @@
             mSpaceState = SPACE_STATE_NONE;
 
             if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
-                resetEntireInputState();
+                resetEntireInputState(newSelStart);
             }
 
             mHandler.postUpdateShiftState();
@@ -841,7 +888,7 @@
      */
     @Override
     public void onExtractedTextClicked() {
-        if (isSuggestionsRequested()) return;
+        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
 
         super.onExtractedTextClicked();
     }
@@ -856,8 +903,8 @@
      * cause the suggestions strip to disappear and re-appear.
      */
     @Override
-    public void onExtractedCursorMovement(int dx, int dy) {
-        if (isSuggestionsRequested()) return;
+    public void onExtractedCursorMovement(final int dx, final int dy) {
+        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
 
         super.onExtractedCursorMovement(dx, dy);
     }
@@ -876,7 +923,7 @@
     }
 
     @Override
-    public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
+    public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
         if (DEBUG) {
             Log.i(TAG, "Received completions:");
             if (applicationSpecifiedCompletions != null) {
@@ -885,43 +932,46 @@
                 }
             }
         }
+        if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
+        mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+        if (applicationSpecifiedCompletions == null) {
+            clearSuggestionStrip();
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.latinIME_onDisplayCompletions(null);
+            }
+            return;
+        }
+
+        final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
+                SuggestedWords.getFromApplicationSpecifiedCompletions(
+                        applicationSpecifiedCompletions);
+        final SuggestedWords suggestedWords = new SuggestedWords(
+                applicationSuggestedWords,
+                false /* typedWordValid */,
+                false /* hasAutoCorrectionCandidate */,
+                false /* isPunctuationSuggestions */,
+                false /* isObsoleteSuggestions */,
+                false /* isPrediction */);
+        // When in fullscreen mode, show completions generated by the application
+        final boolean isAutoCorrection = false;
+        setSuggestionStrip(suggestedWords, isAutoCorrection);
+        setAutoCorrectionIndicator(isAutoCorrection);
+        // TODO: is this the right thing to do? What should we auto-correct to in
+        // this case? This says to keep whatever the user typed.
+        mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
+        setSuggestionStripShown(true);
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
         }
-        if (mInputAttributes.mApplicationSpecifiedCompletionOn) {
-            mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
-            if (applicationSpecifiedCompletions == null) {
-                clearSuggestions();
-                return;
-            }
-
-            final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
-                    SuggestedWords.getFromApplicationSpecifiedCompletions(
-                            applicationSpecifiedCompletions);
-            final SuggestedWords suggestedWords = new SuggestedWords(
-                    applicationSuggestedWords,
-                    false /* typedWordValid */,
-                    false /* hasAutoCorrectionCandidate */,
-                    false /* allowsToBeAutoCorrected */,
-                    false /* isPunctuationSuggestions */,
-                    false /* isObsoleteSuggestions */,
-                    false /* isPrediction */);
-            // When in fullscreen mode, show completions generated by the application
-            final boolean isAutoCorrection = false;
-            setSuggestions(suggestedWords, isAutoCorrection);
-            setAutoCorrectionIndicator(isAutoCorrection);
-            // TODO: is this the right thing to do? What should we auto-correct to in
-            // this case? This says to keep whatever the user typed.
-            mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
-            setSuggestionStripShown(true);
-        }
     }
 
-    private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
+    private void setSuggestionStripShownInternal(final boolean shown,
+            final boolean needsInputViewShown) {
         // TODO: Modify this if we support suggestions with hard keyboard
         if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
-            final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
-            final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
+            final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+            final boolean inputViewShown = (mainKeyboardView != null)
+                    ? mainKeyboardView.isShown() : false;
             final boolean shouldShowSuggestions = shown
                     && (needsInputViewShown ? inputViewShown : true);
             if (isFullscreenMode()) {
@@ -934,7 +984,7 @@
         }
     }
 
-    private void setSuggestionStripShown(boolean shown) {
+    private void setSuggestionStripShown(final boolean shown) {
         setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
     }
 
@@ -944,11 +994,11 @@
             return currentHeight;
         }
 
-        final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
-        if (keyboardView == null) {
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView == null) {
             return 0;
         }
-        final int keyboardHeight = keyboardView.getHeight();
+        final int keyboardHeight = mainKeyboardView.getHeight();
         final int suggestionsHeight = mSuggestionsContainer.getHeight();
         final int displayHeight = mResources.getDisplayMetrics().heightPixels;
         final Rect rect = new Rect();
@@ -958,17 +1008,18 @@
                 - keyboardHeight;
 
         final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
-        params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
+        params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
         mKeyPreviewBackingView.setLayoutParams(params);
         return params.height;
     }
 
     @Override
-    public void onComputeInsets(InputMethodService.Insets outInsets) {
+    public void onComputeInsets(final InputMethodService.Insets outInsets) {
         super.onComputeInsets(outInsets);
-        final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
-        if (inputView == null || mSuggestionsContainer == null)
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView == null || mSuggestionsContainer == null) {
             return;
+        }
         final int adjustedBackingHeight = getAdjustedBackingViewHeight();
         final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
         final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
@@ -981,13 +1032,12 @@
         final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
         int touchY = extraHeight;
         // Need to set touchable region only if input view is being shown
-        final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
-        if (keyboardView != null && keyboardView.isShown()) {
+        if (mainKeyboardView.isShown()) {
             if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
                 touchY -= suggestionsHeight;
             }
-            final int touchWidth = inputView.getWidth();
-            final int touchHeight = inputView.getHeight() + extraHeight
+            final int touchWidth = mainKeyboardView.getWidth();
+            final int touchHeight = mainKeyboardView.getHeight() + extraHeight
                     // Extend touchable region below the keyboard.
                     + EXTENDED_TOUCHABLE_REGION_HEIGHT;
             outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
@@ -1001,7 +1051,7 @@
     public boolean onEvaluateFullscreenMode() {
         // Reread resource value here, because this method is called by framework anytime as needed.
         final boolean isFullscreenModeAllowed =
-                mSettingsValues.isFullscreenModeAllowed(getResources());
+                mCurrentSettings.isFullscreenModeAllowed(getResources());
         return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
     }
 
@@ -1016,15 +1066,15 @@
     }
 
     // This will reset the whole input state to the starting state. It will clear
-    // the composing word, reset the last composed word, tell the inputconnection
-    // and the composingStateManager about it.
-    private void resetEntireInputState() {
+    // the composing word, reset the last composed word, tell the inputconnection about it.
+    private void resetEntireInputState(final int newCursorPosition) {
         resetComposingState(true /* alsoResetLastComposedWord */);
-        updateSuggestions();
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            ic.finishComposingText();
+        if (mCurrentSettings.mBigramPredictionEnabled) {
+            clearSuggestionStrip();
+        } else {
+            setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
         }
+        mConnection.resetCachesUponCursorMove(newCursorPosition);
     }
 
     private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1033,86 +1083,69 @@
             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
     }
 
-    public void commitTyped(final InputConnection ic, final int separatorCode) {
+    private void commitTyped(final String separatorString) {
         if (!mWordComposer.isComposingWord()) return;
         final CharSequence typedWord = mWordComposer.getTypedWord();
         if (typedWord.length() > 0) {
-            if (ic != null) {
-                ic.commitText(typedWord, 1);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_commitText(typedWord);
-                }
-            }
+            mConnection.commitText(typedWord, 1);
             final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
             mLastComposedWord = mWordComposer.commitWord(
                     LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
-                    separatorCode, prevWord);
+                    separatorString, prevWord);
         }
-        updateSuggestions();
     }
 
+    // Called from the KeyboardSwitcher which needs to know auto caps state to display
+    // the right layout.
     public int getCurrentAutoCapsState() {
-        if (!mSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+        if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
 
         final EditorInfo ei = getCurrentInputEditorInfo();
         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
-
         final int inputType = ei.inputType;
-        if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
-            return TextUtils.CAP_MODE_CHARACTERS;
-        }
-
-        final boolean noNeedToCheckCapsMode = (inputType & (InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
-                | InputType.TYPE_TEXT_FLAG_CAP_WORDS)) == 0;
-        if (noNeedToCheckCapsMode) return Constants.TextUtils.CAP_MODE_OFF;
-
-        // Avoid making heavy round-trip IPC calls of {@link InputConnection#getCursorCapsMode}
-        // unless needed.
-        if (mWordComposer.isComposingWord()) return Constants.TextUtils.CAP_MODE_OFF;
-
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return Constants.TextUtils.CAP_MODE_OFF;
-        // TODO: This blocking IPC call is heavy. Consider doing this without using IPC calls.
-        // Note: getCursorCapsMode() returns the current capitalization mode that is any
-        // combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none
-        // of them.
-        return ic.getCursorCapsMode(inputType);
+        // Warning: this depends on mSpaceState, which may not be the most current value. If
+        // mSpaceState gets updated later, whoever called this may need to be told about it.
+        return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(),
+                SPACE_STATE_PHANTOM == mSpaceState);
     }
 
-    // "ic" may be null
-    private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
-        if (null == ic) return;
-        CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
+    // 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) {
-            ic.deleteSurroundingText(2, 0);
+            mConnection.deleteSurroundingText(2, 0);
+            mConnection.commitText(lastTwo.charAt(1) + " ", 1);
             if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_deleteSurroundingText(2);
-            }
-            ic.commitText(lastTwo.charAt(1) + " ", 1);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
+                ResearchLogger.latinIME_swapSwapperAndSpace();
             }
             mKeyboardSwitcher.updateShiftState();
         }
     }
 
-    private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
-        if (mCorrectionMode == Suggest.CORRECTION_NONE) return false;
-        if (ic == null) return false;
-        final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
+    private boolean maybeDoubleSpace() {
+        if (!mCurrentSettings.mCorrectionEnabled) return false;
+        if (!mHandler.isAcceptingDoubleSpaces()) return false;
+        final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
         if (lastThree != null && lastThree.length() == 3
                 && canBeFollowedByPeriod(lastThree.charAt(0))
                 && lastThree.charAt(1) == Keyboard.CODE_SPACE
-                && lastThree.charAt(2) == Keyboard.CODE_SPACE
-                && mHandler.isAcceptingDoubleSpaces()) {
+                && lastThree.charAt(2) == Keyboard.CODE_SPACE) {
             mHandler.cancelDoubleSpacesTimer();
-            ic.deleteSurroundingText(2, 0);
-            ic.commitText(". ", 1);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_doubleSpaceAutoPeriod();
-            }
+            mConnection.deleteSurroundingText(2, 0);
+            mConnection.commitText(". ", 1);
             mKeyboardSwitcher.updateShiftState();
             return true;
         }
@@ -1131,33 +1164,15 @@
                 || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
     }
 
-    // "ic" may be null
-    private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
-        if (ic == null) return;
-        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
-        if (lastOne != null && lastOne.length() == 1
-                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
-            ic.deleteSurroundingText(1, 0);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_deleteSurroundingText(1);
-            }
-        }
-    }
-
+    // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
+    // pressed.
     @Override
-    public boolean addWordToDictionary(String word) {
-        if (USE_BINARY_USER_DICTIONARY) {
-            ((UserBinaryDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
-        } else {
-            ((UserDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
-        }
-        // Suggestion strip should be updated after the operation of adding word to the
-        // user dictionary
-        mHandler.postUpdateSuggestions();
+    public boolean addWordToUserDictionary(final String word) {
+        mUserDictionary.addWordToUserDictionary(word, 128);
         return true;
     }
 
-    private static boolean isAlphabet(int code) {
+    private static boolean isAlphabet(final int code) {
         return Character.isLetter(code);
     }
 
@@ -1170,7 +1185,7 @@
     public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
 
     @Override
-    public boolean onCustomRequest(int requestCode) {
+    public boolean onCustomRequest(final int requestCode) {
         if (isShowingOptionDialog()) return false;
         switch (requestCode) {
         case CODE_SHOW_INPUT_METHOD_PICKER:
@@ -1188,22 +1203,16 @@
         return mOptionsDialog != null && mOptionsDialog.isShowing();
     }
 
-    private static int getActionId(Keyboard keyboard) {
+    private static int getActionId(final Keyboard keyboard) {
         return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
     }
 
-    private void performEditorAction(int actionId) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            ic.performEditorAction(actionId);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_performEditorAction(actionId);
-            }
-        }
+    private void performEditorAction(final int actionId) {
+        mConnection.performEditorAction(actionId);
     }
 
     private void handleLanguageSwitchKey() {
-        final boolean includesOtherImes = mSettingsValues.mIncludesOtherImesInLanguageSwitchList;
+        final boolean includesOtherImes = mCurrentSettings.mIncludesOtherImesInLanguageSwitchList;
         final IBinder token = getWindow().getWindow().getAttributes().token;
         if (mShouldSwitchToLastSubtype) {
             final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
@@ -1221,60 +1230,51 @@
         }
     }
 
-    static private void sendUpDownEnterOrBackspace(final int code, final InputConnection ic) {
+    private void sendDownUpKeyEventForBackwardCompatibility(final int code) {
         final long eventTime = SystemClock.uptimeMillis();
-        ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
+        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
                 KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
-        ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
                 KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
     }
 
-    private void sendKeyCodePoint(int code) {
+    private void sendKeyCodePoint(final int code) {
         // TODO: Remove this special handling of digit letters.
         // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
         if (code >= '0' && code <= '9') {
-            super.sendKeyChar((char)code);
-            return;
-        }
-
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
-            // we want to be able to compile against the Ice Cream Sandwich SDK.
-            if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
-                    && mTargetApplicationInfo.targetSdkVersion < 16) {
-                // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
-                // a hardware keyboard event on pressing enter or delete. This is bad for many
-                // reasons (there are race conditions with commits) but some applications are
-                // relying on this behavior so we continue to support it for older apps.
-                sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER, ic);
-            } else {
-                final String text = new String(new int[] { code }, 0, 1);
-                ic.commitText(text, text.length());
-            }
+            sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0);
             if (ProductionFlag.IS_EXPERIMENTAL) {
                 ResearchLogger.latinIME_sendKeyCodePoint(code);
             }
+            return;
+        }
+
+        // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
+        // we want to be able to compile against the Ice Cream Sandwich SDK.
+        if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
+                && mTargetApplicationInfo.targetSdkVersion < 16) {
+            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
+            // a hardware keyboard event on pressing enter or delete. This is bad for many
+            // reasons (there are race conditions with commits) but some applications are
+            // relying on this behavior so we continue to support it for older apps.
+            sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER);
+        } else {
+            final String text = new String(new int[] { code }, 0, 1);
+            mConnection.commitText(text, text.length());
         }
     }
 
     // Implementation of {@link KeyboardActionListener}.
     @Override
-    public void onCodeInput(int primaryCode, int x, int y) {
+    public void onCodeInput(final int primaryCode, final int x, final int y) {
         final long when = SystemClock.uptimeMillis();
         if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
             mDeleteCount = 0;
         }
         mLastKeyTime = when;
-
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            if (ResearchLogger.sIsLogging) {
-                ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y);
-            }
-        }
-
+        mConnection.beginBatchEdit();
         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
@@ -1307,7 +1307,7 @@
             onSettingsKeyPressed();
             break;
         case Keyboard.CODE_SHORTCUT:
-            mSubtypeSwitcher.switchToShortcutIME();
+            mSubtypeSwitcher.switchToShortcutIME(this);
             break;
         case Keyboard.CODE_ACTION_ENTER:
             performEditorAction(getActionId(switcher.getKeyboard()));
@@ -1321,23 +1321,35 @@
         case Keyboard.CODE_LANGUAGE_SWITCH:
             handleLanguageSwitchKey();
             break;
-        default:
-            if (primaryCode == Keyboard.CODE_TAB
-                    && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
-                performEditorAction(EditorInfo.IME_ACTION_NEXT);
-                break;
+        case Keyboard.CODE_RESEARCH:
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.getInstance().onResearchKeySelected(this);
             }
+            break;
+        default:
             mSpaceState = SPACE_STATE_NONE;
-            if (mSettingsValues.isWordSeparator(primaryCode)) {
+            if (mCurrentSettings.isWordSeparator(primaryCode)) {
                 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
             } else {
+                if (SPACE_STATE_PHANTOM == spaceState) {
+                    if (ProductionFlag.IS_INTERNAL) {
+                        if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
+                            Stats.onAutoCorrection(
+                                    "", mWordComposer.getTypedWord(), " ", mWordComposer);
+                        }
+                    }
+                    commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+                }
+                final int keyX, keyY;
                 final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
                 if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
-                    handleCharacter(primaryCode, x, y, spaceState);
+                    keyX = x;
+                    keyY = y;
                 } else {
-                    handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
-                            spaceState);
+                    keyX = Constants.NOT_A_COORDINATE;
+                    keyY = Constants.NOT_A_COORDINATE;
                 }
+                handleCharacter(primaryCode, keyX, keyY, spaceState);
             }
             mExpectingUpdateSelection = true;
             mShouldSwitchToLastSubtype = true;
@@ -1348,34 +1360,172 @@
         if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT
                 && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
             mLastComposedWord.deactivate();
-        mEnteredText = null;
+        if (Keyboard.CODE_DELETE != primaryCode) {
+            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 text) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return;
-        ic.beginBatchEdit();
-        commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
-        text = specificTldProcessingOnTextInput(ic, text);
+    public void onTextInput(final CharSequence rawText) {
+        mConnection.beginBatchEdit();
+        if (mWordComposer.isComposingWord()) {
+            commitCurrentAutoCorrection(rawText.toString());
+        } else {
+            resetComposingState(true /* alsoResetLastComposedWord */);
+        }
+        mHandler.postUpdateSuggestionStrip();
+        final CharSequence text = specificTldProcessingOnTextInput(rawText);
         if (SPACE_STATE_PHANTOM == mSpaceState) {
             sendKeyCodePoint(Keyboard.CODE_SPACE);
         }
-        ic.commitText(text, 1);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_commitText(text);
-        }
-        ic.endBatchEdit();
+        mConnection.commitText(text, 1);
+        mConnection.endBatchEdit();
+        // Space state must be updated before calling updateShiftState
+        mSpaceState = SPACE_STATE_NONE;
         mKeyboardSwitcher.updateShiftState();
         mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
-        mSpaceState = SPACE_STATE_NONE;
         mEnteredText = text;
-        resetComposingState(true /* alsoResetLastComposedWord */);
     }
 
-    // ic may not be null
-    private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
-            final CharSequence text) {
+    @Override
+    public void onStartBatchInput() {
+        mConnection.beginBatchEdit();
+        if (mWordComposer.isComposingWord()) {
+            if (ProductionFlag.IS_INTERNAL) {
+                if (mWordComposer.isBatchMode()) {
+                    Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer);
+                }
+            }
+            if (mWordComposer.size() <= 1) {
+                // We auto-correct the previous (typed, not gestured) string iff it's one character
+                // long. The reason for this is, even in the middle of gesture typing, you'll still
+                // tap one-letter words and you want them auto-corrected (typically, "i" in English
+                // should become "I"). However for any longer word, we assume that the reason for
+                // tapping probably is that the word you intend to type is not in the dictionary,
+                // so we do not attempt to correct, on the assumption that if that was a dictionary
+                // word, the user would probably have gestured instead.
+                commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
+            } else {
+                commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+            }
+            mExpectingUpdateSelection = true;
+            // The following is necessary for the case where the user typed something but didn't
+            // manual pick it and didn't input any separator.
+            mSpaceState = SPACE_STATE_PHANTOM;
+        }
+        mConnection.endBatchEdit();
+        mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+    }
+
+    private static final class BatchInputUpdater implements Handler.Callback {
+        private final Handler mHandler;
+        private LatinIME mLatinIme;
+
+        private BatchInputUpdater() {
+            final HandlerThread handlerThread = new HandlerThread(
+                    BatchInputUpdater.class.getSimpleName());
+            handlerThread.start();
+            mHandler = new Handler(handlerThread.getLooper(), this);
+        }
+
+        // Initialization-on-demand holder
+        private static final class OnDemandInitializationHolder {
+            public static final BatchInputUpdater sInstance = new BatchInputUpdater();
+        }
+
+        public static BatchInputUpdater getInstance() {
+            return OnDemandInitializationHolder.sInstance;
+        }
+
+        private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
+
+        @Override
+        public boolean handleMessage(final Message msg) {
+            switch (msg.what) {
+            case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
+                final SuggestedWords suggestedWords = getSuggestedWordsGesture(
+                        (InputPointers)msg.obj, mLatinIme);
+                showGesturePreviewAndSuggestionStrip(
+                        suggestedWords, false /* dismissGestureFloatingPreviewText */, mLatinIme);
+                break;
+            }
+            return true;
+        }
+
+        public void updateGesturePreviewAndSuggestionStrip(final InputPointers batchPointers,
+                final LatinIME latinIme) {
+            mLatinIme = latinIme;
+            if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
+                return;
+            }
+            mHandler.obtainMessage(
+                    MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers)
+                    .sendToTarget();
+        }
+
+        public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
+                final boolean dismissGestureFloatingPreviewText, final LatinIME latinIme) {
+            latinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                    suggestedWords, dismissGestureFloatingPreviewText);
+        }
+
+        // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
+        // be synchronized.
+        public synchronized SuggestedWords getSuggestedWordsGesture(
+                final InputPointers batchPointers, final LatinIME latinIme) {
+            latinIme.mWordComposer.setBatchInputPointers(batchPointers);
+            return latinIme.getSuggestedWords(Suggest.SESSION_GESTURE);
+        }
+    }
+
+    private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
+            final boolean dismissGestureFloatingPreviewText) {
+        final String batchInputText = (suggestedWords.size() > 0)
+                ? suggestedWords.getWord(0) : null;
+        final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        mainKeyboardView.showGestureFloatingPreviewText(batchInputText);
+        showSuggestionStrip(suggestedWords, null);
+        if (dismissGestureFloatingPreviewText) {
+            mainKeyboardView.dismissGestureFloatingPreviewText();
+        }
+    }
+
+    @Override
+    public void onUpdateBatchInput(final InputPointers batchPointers) {
+        BatchInputUpdater.getInstance().updateGesturePreviewAndSuggestionStrip(batchPointers, this);
+    }
+
+    @Override
+    public void onEndBatchInput(final InputPointers batchPointers) {
+        final BatchInputUpdater batchInputUpdater = BatchInputUpdater.getInstance();
+        final SuggestedWords suggestedWords = batchInputUpdater.getSuggestedWordsGesture(
+                batchPointers, this);
+        batchInputUpdater.showGesturePreviewAndSuggestionStrip(
+                suggestedWords, true /* dismissGestureFloatingPreviewText */, this);
+        final String batchInputText = (suggestedWords.size() > 0)
+                ? suggestedWords.getWord(0) : null;
+        if (TextUtils.isEmpty(batchInputText)) {
+            return;
+        }
+        mWordComposer.setBatchInputWord(batchInputText);
+        mConnection.beginBatchEdit();
+        if (SPACE_STATE_PHANTOM == mSpaceState) {
+            sendKeyCodePoint(Keyboard.CODE_SPACE);
+        }
+        mConnection.setComposingText(batchInputText, 1);
+        mExpectingUpdateSelection = true;
+        mConnection.endBatchEdit();
+        // Space state must be updated before calling updateShiftState
+        mSpaceState = SPACE_STATE_PHANTOM;
+        mKeyboardSwitcher.updateShiftState();
+    }
+
+    private CharSequence specificTldProcessingOnTextInput(final CharSequence text) {
         if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
                 || !Character.isLetter(text.charAt(1))) {
             // Not a tld: do nothing.
@@ -1384,7 +1534,7 @@
         // We have a TLD (or something that looks like this): make sure we don't add
         // a space even if currently in phantom mode.
         mSpaceState = SPACE_STATE_NONE;
-        final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
+        final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
         if (lastOne != null && lastOne.length() == 1
                 && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
             return text.subSequence(1, text.length());
@@ -1393,6 +1543,7 @@
         }
     }
 
+    // Called from PointerTracker through the KeyboardActionListener interface
     @Override
     public void onCancelInput() {
         // User released a finger outside any key
@@ -1400,67 +1551,52 @@
     }
 
     private void handleBackspace(final int spaceState) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return;
-        ic.beginBatchEdit();
-        handleBackspaceWhileInBatchEdit(spaceState, ic);
-        ic.endBatchEdit();
-    }
-
-    // "ic" may not be null.
-    private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
         // In many cases, we may have to put the keyboard in auto-shift state again.
         mHandler.postUpdateShiftState();
 
-        if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
-            // Cancel multi-character input: remove the text we just entered.
-            // This is triggered on backspace after a key that inputs multiple characters,
-            // like the smiley key or the .com key.
-            final int length = mEnteredText.length();
-            ic.deleteSurroundingText(length, 0);
-            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.
-            return;
-        }
-
         if (mWordComposer.isComposingWord()) {
             final int length = mWordComposer.size();
             if (length > 0) {
-                mWordComposer.deleteLast();
-                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
-                // If we have deleted the last remaining character of a word, then we are not
-                // isComposingWord() any more.
-                if (!mWordComposer.isComposingWord()) {
-                    // Not composing word any more, so we can show bigrams.
-                    mHandler.postUpdateBigramPredictions();
+                // Immediately after a batch input.
+                if (SPACE_STATE_PHANTOM == spaceState) {
+                    mWordComposer.reset();
                 } else {
-                    // Still composing a word, so we still have letters to deduce a suggestion from.
-                    mHandler.postUpdateSuggestions();
+                    mWordComposer.deleteLast();
                 }
+                mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+                mHandler.postUpdateSuggestionStrip();
             } else {
-                ic.deleteSurroundingText(1, 0);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_deleteSurroundingText(1);
-                }
+                mConnection.deleteSurroundingText(1, 0);
             }
         } else {
             if (mLastComposedWord.canRevertCommit()) {
-                Utils.Stats.onAutoCorrectionCancellation();
-                revertCommit(ic);
+                if (ProductionFlag.IS_INTERNAL) {
+                    Stats.onAutoCorrectionCancellation();
+                }
+                revertCommit();
+                return;
+            }
+            if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
+                // Cancel multi-character input: remove the text we just entered.
+                // This is triggered on backspace after a key that inputs multiple characters,
+                // like the smiley key or the .com key.
+                final int length = mEnteredText.length();
+                mConnection.deleteSurroundingText(length, 0);
+                mEnteredText = null;
+                // 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.
                 return;
             }
             if (SPACE_STATE_DOUBLE == spaceState) {
-                if (revertDoubleSpaceWhileInBatchEdit(ic)) {
+                mHandler.cancelDoubleSpacesTimer();
+                if (mConnection.revertDoubleSpace()) {
                     // No need to reset mSpaceState, it has already be done (that's why we
                     // receive it as a parameter)
                     return;
                 }
             } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
-                if (revertSwapPunctuation(ic)) {
+                if (mConnection.revertSwapPunctuation()) {
                     // Likewise
                     return;
                 }
@@ -1471,11 +1607,8 @@
             if (mLastSelectionStart != mLastSelectionEnd) {
                 // If there is a selection, remove it.
                 final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
-                ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
-                ic.deleteSurroundingText(lengthToDelete, 0);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
-                }
+                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+                mConnection.deleteSurroundingText(lengthToDelete, 0);
             } else {
                 // There is no selection, just delete one character.
                 if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
@@ -1490,40 +1623,33 @@
                     // a hardware keyboard event on pressing enter or delete. This is bad for many
                     // reasons (there are race conditions with commits) but some applications are
                     // relying on this behavior so we continue to support it for older apps.
-                    sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL, ic);
+                    sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
                 } else {
-                    ic.deleteSurroundingText(1, 0);
-                }
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_deleteSurroundingText(1);
+                    mConnection.deleteSurroundingText(1, 0);
                 }
                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
-                    ic.deleteSurroundingText(1, 0);
-                    if (ProductionFlag.IS_EXPERIMENTAL) {
-                        ResearchLogger.latinIME_deleteSurroundingText(1);
-                    }
+                    mConnection.deleteSurroundingText(1, 0);
                 }
             }
-            if (isSuggestionsRequested()) {
-                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
+            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
             }
         }
     }
 
-    // ic may be null
-    private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
+    private boolean maybeStripSpace(final int code,
             final int spaceState, final boolean isFromSuggestionStrip) {
         if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
-            removeTrailingSpaceWhileInBatchEdit(ic);
+            mConnection.removeTrailingSpace();
             return false;
         } else if ((SPACE_STATE_WEAK == spaceState
                 || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
                 && isFromSuggestionStrip) {
-            if (mSettingsValues.isWeakSpaceSwapper(code)) {
+            if (mCurrentSettings.isWeakSpaceSwapper(code)) {
                 return true;
             } else {
-                if (mSettingsValues.isWeakSpaceStripper(code)) {
-                    removeTrailingSpaceWhileInBatchEdit(ic);
+                if (mCurrentSettings.isWeakSpaceStripper(code)) {
+                    mConnection.removeTrailingSpace();
                 }
                 return false;
             }
@@ -1534,20 +1660,10 @@
 
     private void handleCharacter(final int primaryCode, final int x,
             final int y, final int spaceState) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (null != ic) ic.beginBatchEdit();
-        // TODO: if ic is null, does it make any sense to call this?
-        handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
-        if (null != ic) ic.endBatchEdit();
-    }
-
-    // "ic" may be null without this crashing, but the behavior will be really strange
-    private void handleCharacterWhileInBatchEdit(final int primaryCode,
-            final int x, final int y, final int spaceState, final InputConnection ic) {
         boolean isComposingWord = mWordComposer.isComposingWord();
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
+                !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) {
             if (isComposingWord) {
                 // Sanity check
                 throw new RuntimeException("Should not be composing here");
@@ -1559,8 +1675,9 @@
         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
         // thread here.
         if (!isComposingWord && (isAlphabet(primaryCode)
-                || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
-                && isSuggestionsRequested() && !isCursorTouchingWord()) {
+                || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode))
+                && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) &&
+                !mConnection.isCursorTouchingWord(mCurrentSettings)) {
             // Reset entirely the composing state anyway, then start composing a new word unless
             // the character is a single quote. The idea here is, single quote is not a
             // separator and it should be treated as a normal character, except in the first
@@ -1571,85 +1688,71 @@
             // it entirely and resume suggestions on the previous word, we'd like to still
             // have touch coordinates for it.
             resetComposingState(false /* alsoResetLastComposedWord */);
-            clearSuggestions();
         }
         if (isComposingWord) {
-            mWordComposer.add(
-                    primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
-            if (ic != null) {
-                // If it's the first letter, make note of auto-caps state
-                if (mWordComposer.size() == 1) {
-                    mWordComposer.setAutoCapitalized(
-                            getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
-                }
-                ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+            final int keyX, keyY;
+            if (KeyboardActionListener.Adapter.isInvalidCoordinate(x)
+                    || KeyboardActionListener.Adapter.isInvalidCoordinate(y)) {
+                keyX = x;
+                keyY = y;
+            } else {
+                final KeyDetector keyDetector =
+                        mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
+                keyX = keyDetector.getTouchX(x);
+                keyY = keyDetector.getTouchY(y);
             }
-            mHandler.postUpdateSuggestions();
+            mWordComposer.add(primaryCode, keyX, keyY);
+            // If it's the first letter, make note of auto-caps state
+            if (mWordComposer.size() == 1) {
+                mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+            }
+            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
         } else {
-            final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
-                    spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+            final boolean swapWeakSpace = maybeStripSpace(primaryCode,
+                    spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
 
             sendKeyCodePoint(primaryCode);
 
             if (swapWeakSpace) {
-                swapSwapperAndSpaceWhileInBatchEdit(ic);
+                swapSwapperAndSpace();
                 mSpaceState = SPACE_STATE_WEAK;
             }
-            // Some characters are not word separators, yet they don't start a new
-            // composing span. For these, we haven't changed the suggestion strip, and
-            // if the "add to dictionary" hint is shown, we should do so now. Examples of
-            // such characters include single quote, dollar, and others; the exact list is
-            // the list of characters for which we enter handleCharacterWhileInBatchEdit
-            // that don't match the test if ((isAlphabet...)) at the top of this method.
-            if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
-                mHandler.postUpdateBigramPredictions();
-            }
+            // In case the "add to dictionary" hint was still displayed.
+            if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
         }
-        Utils.Stats.onNonSeparator((char)primaryCode, x, y);
+        mHandler.postUpdateSuggestionStrip();
+        if (ProductionFlag.IS_INTERNAL) {
+            Utils.Stats.onNonSeparator((char)primaryCode, x, y);
+        }
     }
 
     // Returns true if we did an autocorrection, false otherwise.
     private boolean handleSeparator(final int primaryCode, final int x, final int y,
             final int spaceState) {
-        // Should dismiss the "Touch again to save" message when handling separator
-        if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
-            mHandler.cancelUpdateBigramPredictions();
-            mHandler.postUpdateSuggestions();
-        }
-
         boolean didAutoCorrect = false;
         // Handle separator
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            ic.beginBatchEdit();
-        }
         if (mWordComposer.isComposingWord()) {
-            // In certain languages where single quote is a separator, it's better
-            // not to auto correct, but accept the typed word. For instance,
-            // in Italian dov' should not be expanded to dove' because the elision
-            // requires the last vowel to be removed.
-            final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
-                    && !mInputAttributes.mInputTypeNoAutoCorrect;
-            if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
-                commitCurrentAutoCorrection(primaryCode, ic);
+            if (mCurrentSettings.mCorrectionEnabled) {
+                // TODO: maybe cache Strings in an <String> sparse array or something
+                commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1));
                 didAutoCorrect = true;
             } else {
-                commitTyped(ic, primaryCode);
+                commitTyped(new String(new int[]{primaryCode}, 0, 1));
             }
         }
 
-        final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
-                KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+        final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
+                Constants.SUGGESTION_STRIP_COORDINATE == x);
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
+                mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
             sendKeyCodePoint(Keyboard.CODE_SPACE);
         }
         sendKeyCodePoint(primaryCode);
 
         if (Keyboard.CODE_SPACE == primaryCode) {
-            if (isSuggestionsRequested()) {
-                if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
+            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+                if (maybeDoubleSpace()) {
                     mSpaceState = SPACE_STATE_DOUBLE;
                 } else if (!isShowingPunctuationList()) {
                     mSpaceState = SPACE_STATE_WEAK;
@@ -1657,21 +1760,26 @@
             }
 
             mHandler.startDoubleSpacesTimer();
-            if (!isCursorTouchingWord()) {
-                mHandler.cancelUpdateSuggestions();
-                mHandler.postUpdateBigramPredictions();
+            if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
+                mHandler.postUpdateSuggestionStrip();
             }
         } else {
             if (swapWeakSpace) {
-                swapSwapperAndSpaceWhileInBatchEdit(ic);
+                swapSwapperAndSpace();
                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
-            } else if (SPACE_STATE_PHANTOM == spaceState) {
+            } else if (SPACE_STATE_PHANTOM == spaceState
+                    && !mCurrentSettings.isWeakSpaceStripper(primaryCode)
+                    && !mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
                 // If we are in phantom space state, and the user presses a separator, we want to
                 // stay in phantom space state so that the next keypress has a chance to add the
                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
                 // then insert a comma and go on to typing the next word, I want the space to be
                 // inserted automatically before the next word, the same way it is when I don't
                 // input the comma.
+                // The case is a little different if the separator is a space stripper. Such a
+                // separator does not normally need a space on the right (that's the difference
+                // between swappers and strippers), so we should not stay in phantom space state if
+                // the separator is a stripper. Hence the additional test above.
                 mSpaceState = SPACE_STATE_PHANTOM;
             }
 
@@ -1679,12 +1787,11 @@
             // already displayed or not, so it's okay.
             setPunctuationSuggestions();
         }
-
-        Utils.Stats.onSeparator((char)primaryCode, x, y);
-
-        if (ic != null) {
-            ic.endBatchEdit();
+        if (ProductionFlag.IS_INTERNAL) {
+            Utils.Stats.onSeparator((char)primaryCode, x, y);
         }
+
+        mHandler.postUpdateShiftState();
         return didAutoCorrect;
     }
 
@@ -1695,153 +1802,134 @@
     }
 
     private void handleClose() {
-        commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
+        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
         requestHideSelf(0);
-        LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
-        if (inputView != null)
-            inputView.closing();
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+        if (mainKeyboardView != null) {
+            mainKeyboardView.closing();
+        }
     }
 
-    public boolean isSuggestionsRequested() {
-        return mInputAttributes.mIsSettingsSuggestionStripOn
-                && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
+    // TODO: make this private
+    // Outside LatinIME, only used by the test suite.
+    /* package for tests */
+    boolean isShowingPunctuationList() {
+        if (mSuggestionStripView == null) return false;
+        return mCurrentSettings.mSuggestPuncList == mSuggestionStripView.getSuggestions();
     }
 
-    public boolean isShowingPunctuationList() {
-        if (mSuggestionsView == null) return false;
-        return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
-    }
-
-    public boolean isShowingSuggestionsStrip() {
-        return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
-                || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
-                        && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
-    }
-
-    public boolean isSuggestionsStripVisible() {
-        if (mSuggestionsView == null)
+    private boolean isSuggestionsStripVisible() {
+        if (mSuggestionStripView == null)
             return false;
-        if (mSuggestionsView.isShowingAddToDictionaryHint())
+        if (mSuggestionStripView.isShowingAddToDictionaryHint())
             return true;
-        if (!isShowingSuggestionsStrip())
+        if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
             return false;
-        if (mInputAttributes.mApplicationSpecifiedCompletionOn)
+        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
             return true;
-        return isSuggestionsRequested();
+        return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation);
     }
 
-    public void switchToKeyboardView() {
-        if (DEBUG) {
-            Log.d(TAG, "Switch to keyboard view.");
-        }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_switchToKeyboardView();
-        }
-        View v = mKeyboardSwitcher.getKeyboardView();
-        if (v != null) {
-            // Confirms that the keyboard view doesn't have parent view.
-            ViewParent p = v.getParent();
-            if (p != null && p instanceof ViewGroup) {
-                ((ViewGroup) p).removeView(v);
-            }
-            setInputView(v);
-        }
-        setSuggestionStripShown(isSuggestionsStripVisible());
-        updateInputViewShown();
-        mHandler.postUpdateSuggestions();
-    }
-
-    public void clearSuggestions() {
-        setSuggestions(SuggestedWords.EMPTY, false);
+    private void clearSuggestionStrip() {
+        setSuggestionStrip(SuggestedWords.EMPTY, false);
         setAutoCorrectionIndicator(false);
     }
 
-    private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
-        if (mSuggestionsView != null) {
-            mSuggestionsView.setSuggestions(words);
+    private void setSuggestionStrip(final SuggestedWords words, final boolean isAutoCorrection) {
+        if (mSuggestionStripView != null) {
+            mSuggestionStripView.setSuggestions(words);
             mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
         }
     }
 
     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
         // Put a blue underline to a word in TextView which will be auto-corrected.
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return;
         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
                 && mWordComposer.isComposingWord()) {
             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
             final CharSequence textWithUnderline =
                     getTextWithUnderline(mWordComposer.getTypedWord());
-            ic.setComposingText(textWithUnderline, 1);
+            mConnection.setComposingText(textWithUnderline, 1);
         }
     }
 
-    public void updateSuggestions() {
+    private void updateSuggestionStrip() {
+        mHandler.cancelUpdateSuggestionStrip();
+
         // Check if we have a suggestion engine attached.
-        if ((mSuggest == null || !isSuggestionsRequested())) {
+        if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
             if (mWordComposer.isComposingWord()) {
-                Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
+                Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
+                        + "requested!");
                 mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
             }
             return;
         }
 
-        mHandler.cancelUpdateSuggestions();
-        mHandler.cancelUpdateBigramPredictions();
-
-        if (!mWordComposer.isComposingWord()) {
+        if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) {
             setPunctuationSuggestions();
             return;
         }
 
-        // TODO: May need a better way of retrieving previous word
-        final InputConnection ic = getCurrentInputConnection();
-        final CharSequence prevWord;
-        if (null == ic) {
-            prevWord = null;
-        } else {
-            prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
-        }
+        final SuggestedWords suggestedWords = getSuggestedWords(Suggest.SESSION_TYPING);
+        final String typedWord = mWordComposer.getTypedWord();
+        showSuggestionStrip(suggestedWords, typedWord);
+    }
 
-        final CharSequence typedWord = mWordComposer.getTypedWord();
-        // getSuggestedWords handles gracefully a null value of prevWord
+    private SuggestedWords getSuggestedWords(final int sessionId) {
+        final String typedWord = mWordComposer.getTypedWord();
+        // Get the word on which we should search the bigrams. If we are composing a word, it's
+        // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
+        // should just skip whitespace if any, so 1.
+        // TODO: this is slow (2-way IPC) - we should probably cache this instead.
+        final CharSequence prevWord =
+                mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators,
+                mWordComposer.isComposingWord() ? 2 : 1);
         final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
-                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
+                prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
+                mCurrentSettings.mCorrectionEnabled, sessionId);
+        return maybeRetrieveOlderSuggestions(typedWord, suggestedWords);
+    }
 
-        // Basically, we update the suggestion strip only when suggestion count > 1.  However,
-        // there is an exception: We update the suggestion strip whenever typed word's length
-        // is 1 or typed word is found in dictionary, regardless of suggestion count.  Actually,
-        // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
-        // need to clear the previous state when the user starts typing a word (i.e. typed word's
-        // length == 1).
-        if (suggestedWords.size() > 1 || typedWord.length() == 1
-                || !suggestedWords.mAllowsToBeAutoCorrected
-                || mSuggestionsView.isShowingAddToDictionaryHint()) {
-            showSuggestions(suggestedWords, typedWord);
+    private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord,
+            final SuggestedWords suggestedWords) {
+        // TODO: consolidate this into getSuggestedWords
+        // We update the suggestion strip only when we have some suggestions to show, i.e. when
+        // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
+        // replaced with the new one. However, when the word is a dictionary word, or when the
+        // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
+        // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
+        // revert to suggestions - although it is unclear how we can come here if it's displayed.
+        if (suggestedWords.size() > 1 || typedWord.length() <= 1
+                || !suggestedWords.mTypedWordValid
+                || mSuggestionStripView.isShowingAddToDictionaryHint()) {
+            return suggestedWords;
         } else {
-            SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
-            if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
+            SuggestedWords previousSuggestions = mSuggestionStripView.getSuggestions();
+            if (previousSuggestions == mCurrentSettings.mSuggestPuncList) {
                 previousSuggestions = SuggestedWords.EMPTY;
             }
             final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
                     SuggestedWords.getTypedWordAndPreviousSuggestions(
                             typedWord, previousSuggestions);
-            final SuggestedWords obsoleteSuggestedWords =
-                    new SuggestedWords(typedWordAndPreviousSuggestions,
+            return new SuggestedWords(typedWordAndPreviousSuggestions,
                             false /* typedWordValid */,
                             false /* hasAutoCorrectionCandidate */,
-                            false /* allowsToBeAutoCorrected */,
                             false /* isPunctuationSuggestions */,
                             true /* isObsoleteSuggestions */,
                             false /* isPrediction */);
-            showSuggestions(obsoleteSuggestedWords, typedWord);
         }
     }
 
-    public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
+    private void showSuggestionStrip(final SuggestedWords suggestedWords,
+            final CharSequence typedWord) {
+        if (null == suggestedWords || suggestedWords.size() <= 0) {
+            clearSuggestionStrip();
+            return;
+        }
         final CharSequence autoCorrection;
         if (suggestedWords.size() > 0) {
-            if (suggestedWords.hasAutoCorrectionWord()) {
+            if (suggestedWords.mWillAutoCorrect) {
                 autoCorrection = suggestedWords.getWord(1);
             } else {
                 autoCorrection = typedWord;
@@ -1851,94 +1939,89 @@
         }
         mWordComposer.setAutoCorrection(autoCorrection);
         final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
-        setSuggestions(suggestedWords, isAutoCorrection);
+        setSuggestionStrip(suggestedWords, isAutoCorrection);
         setAutoCorrectionIndicator(isAutoCorrection);
         setSuggestionStripShown(isSuggestionsStripVisible());
     }
 
-    private void commitCurrentAutoCorrection(final int separatorCodePoint,
-            final InputConnection ic) {
+    private void commitCurrentAutoCorrection(final String separatorString) {
         // Complete any pending suggestions query first
         if (mHandler.hasPendingUpdateSuggestions()) {
-            mHandler.cancelUpdateSuggestions();
-            updateSuggestions();
+            updateSuggestionStrip();
         }
-        final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
+        final CharSequence typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
+        final String typedWord = mWordComposer.getTypedWord();
+        final CharSequence autoCorrection = (typedAutoCorrection != null)
+                ? typedAutoCorrection : typedWord;
         if (autoCorrection != null) {
-            final String typedWord = mWordComposer.getTypedWord();
             if (TextUtils.isEmpty(typedWord)) {
                 throw new RuntimeException("We have an auto-correction but the typed word "
                         + "is empty? Impossible! I must commit suicide.");
             }
-            Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
-                        autoCorrection.toString());
+            if (ProductionFlag.IS_INTERNAL) {
+                Stats.onAutoCorrection(
+                        typedWord, autoCorrection.toString(), separatorString, mWordComposer);
             }
             mExpectingUpdateSelection = true;
             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
-                    separatorCodePoint);
-            if (!typedWord.equals(autoCorrection) && null != ic) {
+                    separatorString);
+            if (!typedWord.equals(autoCorrection)) {
                 // This will make the correction flash for a short while as a visual clue
-                // to the user that auto-correction happened.
-                ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+                // to the user that auto-correction happened. It has no other effect; in particular
+                // note that this won't affect the text inside the text field AT ALL: it only makes
+                // the segment of text starting at the supplied index and running for the length
+                // of the auto-correction flash. At this moment, the "typedWord" argument is
+                // ignored by TextView.
+                mConnection.commitCorrection(
+                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
                         typedWord, autoCorrection));
             }
         }
     }
 
+    // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
+    // interface
     @Override
-    public void pickSuggestionManually(final int index, final CharSequence suggestion,
-            int x, int y) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (null != ic) ic.beginBatchEdit();
-        pickSuggestionManuallyWhileInBatchEdit(index, suggestion, x, y, ic);
-        if (null != ic) ic.endBatchEdit();
-    }
-
-    public void pickSuggestionManuallyWhileInBatchEdit(final int index,
-        final CharSequence suggestion, final int x, final int y, final InputConnection ic) {
-        final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
+    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()) {
             // Word separators are suggested before the user inputs something.
             // 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;
         }
 
-        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
+        mConnection.beginBatchEdit();
+        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
+                // In the batch input mode, a manually picked suggested word should just replace
+                // the current batch input text and there is no need for a phantom space.
+                && !mWordComposer.isBatchMode()) {
             int firstChar = Character.codePointAt(suggestion, 0);
-            if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
-                    && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
+            if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
+                    && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
                 sendKeyCodePoint(Keyboard.CODE_SPACE);
             }
         }
 
-        if (mInputAttributes.mApplicationSpecifiedCompletionOn
+        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
                 && mApplicationSpecifiedCompletions != null
                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
-            if (mSuggestionsView != null) {
-                mSuggestionsView.clear();
+            if (mSuggestionStripView != null) {
+                mSuggestionStripView.clear();
             }
             mKeyboardSwitcher.updateShiftState();
             resetComposingState(true /* alsoResetLastComposedWord */);
-            if (ic != null) {
-                final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
-                ic.commitCompletion(completionInfo);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
-                            completionInfo.getText(), x, y);
-                }
-            }
+            final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
+            mConnection.commitCompletion(completionInfo);
+            mConnection.endBatchEdit();
             return;
         }
 
@@ -1947,50 +2030,37 @@
         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();
+        // Space state must be updated before calling updateShiftState
         mSpaceState = SPACE_STATE_PHANTOM;
-        // TODO: is this necessary?
         mKeyboardSwitcher.updateShiftState();
 
         // We should show the "Touch again to save" hint if the user pressed the first entry
-        // AND either:
-        // - There is no dictionary (we know that because we tried to load it => null != mSuggest
-        //   AND mSuggest.hasMainDictionary() is false)
-        // - There is a dictionary and the word is not in it
+        // AND it's in none of our current dictionaries (main, user or otherwise).
         // Please note that if mSuggest is null, it means that everything is off: suggestion
         // and correction, so we shouldn't try to show the hint
-        // We used to look at mCorrectionMode here, but showing the hint should have nothing
-        // to do with the autocorrection setting.
         final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
-                // If there is no dictionary the hint should be shown.
-                && (!mSuggest.hasMainDictionary()
-                        // If "suggestion" is not in the dictionary, the hint should be shown.
-                        || !AutoCorrection.isValidWord(
-                                mSuggest.getUnigramDictionaries(), suggestion, true));
+                // 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);
-        if (!showingAddToDictionaryHint) {
-            // If we're not showing the "Touch again to save", then show corrections again.
-            // In case the cursor position doesn't change, make sure we show the suggestions again.
-            updateBigramPredictions();
-            // Updating the predictions right away may be slow and feel unresponsive on slower
-            // terminals. On the other hand if we just postUpdateBigramPredictions() it will
-            // take a noticeable delay to update them which may feel uneasy.
+        if (ProductionFlag.IS_INTERNAL) {
+            Stats.onSeparator((char)Keyboard.CODE_SPACE,
+                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+        }
+        if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
+            mSuggestionStripView.showAddToDictionaryHint(
+                    suggestion, mCurrentSettings.mHintToSaveText);
         } else {
-            if (mIsUserDictionaryAvailable) {
-                mSuggestionsView.showAddToDictionaryHint(
-                        suggestion, mSettingsValues.mHintToSaveText);
-            } else {
-                mHandler.postUpdateSuggestions();
-            }
+            // If we're not showing the "Touch again to save", then update the suggestion strip.
+            mHandler.postUpdateSuggestionStrip();
         }
     }
 
@@ -1998,24 +2068,10 @@
      * Commits the chosen word to the text field and saves it for later retrieval.
      */
     private void commitChosenWord(final CharSequence chosenWord, final int commitType,
-            final int separatorCode) {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic != null) {
-            if (mSettingsValues.mEnableSuggestionSpanInsertion) {
-                final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
-                ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
-                        this, chosenWord, suggestedWords, mIsMainDictionaryAvailable),
-                        1);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_commitText(chosenWord);
-                }
-            } else {
-                ic.commitText(chosenWord, 1);
-                if (ProductionFlag.IS_EXPERIMENTAL) {
-                    ResearchLogger.latinIME_commitText(chosenWord);
-                }
-            }
-        }
+            final String separatorString) {
+        final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions();
+        mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
+                this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
         // 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
@@ -2023,45 +2079,14 @@
         // LastComposedWord#didCommitTypedWord by string equality of the remembered
         // strings.
         mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(),
-                separatorCode, prevWord);
+                separatorString, prevWord);
     }
 
-    public void updateBigramPredictions() {
-        if (mSuggest == null || !isSuggestionsRequested())
-            return;
-
-        if (!mSettingsValues.mBigramPredictionEnabled) {
-            setPunctuationSuggestions();
-            return;
-        }
-
-        final SuggestedWords suggestedWords;
-        if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
-            final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
-                    mSettingsValues.mWordSeparators);
-            if (!TextUtils.isEmpty(prevWord)) {
-                suggestedWords = mSuggest.getBigramPredictions(prevWord);
-            } else {
-                suggestedWords = null;
-            }
+    private void setPunctuationSuggestions() {
+        if (mCurrentSettings.mBigramPredictionEnabled) {
+            clearSuggestionStrip();
         } else {
-            suggestedWords = null;
-        }
-
-        if (null != suggestedWords && suggestedWords.size() > 0) {
-            // Explicitly supply an empty typed word (the no-second-arg version of
-            // showSuggestions will retrieve the word near the cursor, we don't want that here)
-            showSuggestions(suggestedWords, "");
-        } else {
-            clearSuggestions();
-        }
-    }
-
-    public void setPunctuationSuggestions() {
-        if (mSettingsValues.mBigramPredictionEnabled) {
-            clearSuggestions();
-        } else {
-            setSuggestions(mSettingsValues.mSuggestPuncList, false);
+            setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
         }
         setAutoCorrectionIndicator(false);
         setSuggestionStripShown(isSuggestionsStripVisible());
@@ -2069,25 +2094,19 @@
 
     private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
         if (TextUtils.isEmpty(suggestion)) return null;
+        if (mSuggest == null) return null;
 
-        // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
-        // adding words in situations where the user or application really didn't
-        // want corrections enabled or learned.
-        if (!(mCorrectionMode == Suggest.CORRECTION_FULL
-                || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
-            return null;
-        }
+        // If correction is not enabled, we don't add words to the user history dictionary.
+        // That's to avoid unintended additions in some sensitive fields, or fields that
+        // expect to receive non-words.
+        if (!mCurrentSettings.mCorrectionEnabled) return null;
 
-        if (mUserHistoryDictionary != null) {
-            final InputConnection ic = getCurrentInputConnection();
-            final CharSequence prevWord;
-            if (null != ic) {
-                prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
-            } else {
-                prevWord = null;
-            }
+        final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
+        if (userHistoryDictionary != null) {
+            final CharSequence prevWord
+                    = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2);
             final String secondWord;
-            if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
+            if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
                 secondWord = suggestion.toString().toLowerCase(
                         mSubtypeSwitcher.getCurrentSubtypeLocale());
             } else {
@@ -2098,101 +2117,39 @@
             final int maxFreq = AutoCorrection.getMaxFrequency(
                     mSuggest.getUnigramDictionaries(), suggestion);
             if (maxFreq == 0) return null;
-            mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
+            userHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
                     secondWord, maxFreq > 0);
             return prevWord;
         }
         return null;
     }
 
-    public boolean isCursorTouchingWord() {
-        final InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return false;
-        CharSequence before = ic.getTextBeforeCursor(1, 0);
-        CharSequence after = ic.getTextAfterCursor(1, 0);
-        if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))
-                && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
-            return true;
-        }
-        if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))
-                && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
-            return true;
-        }
-        return false;
-    }
-
-    // "ic" must not be null
-    private static boolean sameAsTextBeforeCursor(final InputConnection ic,
-            final CharSequence text) {
-        final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
-        return TextUtils.equals(text, beforeText);
-    }
-
-    // "ic" must not be null
     /**
      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
      * word, else do nothing.
      */
-    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
-            final InputConnection ic) {
-        // Bail out if the cursor is not at the end of a word (cursor must be preceded by
-        // non-whitespace, non-separator, non-start-of-text)
-        // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
-        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
-        if (TextUtils.isEmpty(textBeforeCursor)
-                || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
-
-        // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
-        // separator or end of line/text)
-        // Example: "test|"<EOL> "te|st" get rejected here
-        final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
-        if (!TextUtils.isEmpty(textAfterCursor)
-                && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
-
-        // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
-        // Example: " -|" gets rejected here but "e-|" and "e|" are okay
-        CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
-        // We don't suggest on leading single quotes, so we have to remove them from the word if
-        // it starts with single quotes.
-        while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
-            word = word.subSequence(1, word.length());
+    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
+        final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings);
+        if (null != word) {
+            restartSuggestionsOnWordBeforeCursor(word);
         }
-        if (TextUtils.isEmpty(word)) return;
-        final char firstChar = word.charAt(0); // we just tested that word is not empty
-        if (word.length() == 1 && !Character.isLetter(firstChar)) return;
-
-        // We only suggest on words that start with a letter or a symbol that is excluded from
-        // word separators (see #handleCharacterWhileInBatchEdit).
-        if (!(isAlphabet(firstChar)
-                || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
-            return;
-        }
-
-        // Okay, we are at the end of a word. Restart suggestions.
-        restartSuggestionsOnWordBeforeCursor(ic, word);
     }
 
-    // "ic" must not be null
-    private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
-            final CharSequence word) {
+    private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
         final int length = word.length();
-        ic.deleteSurroundingText(length, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(length);
-        }
-        ic.setComposingText(word, 1);
-        mHandler.postUpdateSuggestions();
+        mConnection.deleteSurroundingText(length, 0);
+        mConnection.setComposingText(word, 1);
+        mHandler.postUpdateSuggestionStrip();
     }
 
-    // "ic" must not be null
-    private void revertCommit(final InputConnection ic) {
+    private void revertCommit() {
         final CharSequence previousWord = mLastComposedWord.mPrevWord;
         final String originallyTypedWord = mLastComposedWord.mTypedWord;
         final CharSequence committedWord = mLastComposedWord.mCommittedWord;
         final int cancelLength = committedWord.length();
         final int separatorLength = LastComposedWord.getSeparatorLength(
-                mLastComposedWord.mSeparatorCode);
+                mLastComposedWord.mSeparatorString);
         // TODO: should we check our saved separator against the actual contents of the text view?
         final int deleteLength = cancelLength + separatorLength;
         if (DEBUG) {
@@ -2200,7 +2157,7 @@
                 throw new RuntimeException("revertCommit, but we are composing a word");
             }
             final String wordBeforeCursor =
-                    ic.getTextBeforeCursor(deleteLength, 0)
+                    mConnection.getTextBeforeCursor(deleteLength, 0)
                             .subSequence(0, cancelLength).toString();
             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
                 throw new RuntimeException("revertCommit check failed: we thought we were "
@@ -2208,132 +2165,67 @@
                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
             }
         }
-        ic.deleteSurroundingText(deleteLength, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
-        }
+        mConnection.deleteSurroundingText(deleteLength, 0);
         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
             mUserHistoryDictionary.cancelAddingUserHistory(
                     previousWord.toString(), committedWord.toString());
         }
-        if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
-            // This is the case when we cancel a manual pick.
-            // We should restart suggestion on the word right away.
-            mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
-            ic.setComposingText(originallyTypedWord, 1);
-        } else {
-            ic.commitText(originallyTypedWord, 1);
-            // Re-insert the separator
-            sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
-            Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
-                    WordComposer.NOT_A_COORDINATE);
-            if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.latinIME_revertCommit(originallyTypedWord);
-            }
-            // Don't restart suggestion yet. We'll restart if the user deletes the
-            // separator.
+        mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1);
+        if (ProductionFlag.IS_INTERNAL) {
+            Stats.onSeparator(mLastComposedWord.mSeparatorString,
+                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         }
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_revertCommit(originallyTypedWord);
+        }
+        // Don't restart suggestion yet. We'll restart if the user deletes the
+        // separator.
         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
-        mHandler.cancelUpdateBigramPredictions();
-        mHandler.postUpdateSuggestions();
+        // We have a separator between the word and the cursor: we should show predictions.
+        mHandler.postUpdateSuggestionStrip();
     }
 
-    // "ic" must not be null
-    private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
-        mHandler.cancelDoubleSpacesTimer();
-        // Here we test whether we indeed have a period and a space before us. This should not
-        // be needed, but it's there just in case something went wrong.
-        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
-        if (!". ".equals(textBeforeCursor)) {
-            // Theoretically we should not be coming here if there isn't ". " before the
-            // cursor, but the application may be changing the text while we are typing, so
-            // anything goes. We should not crash.
-            Log.d(TAG, "Tried to revert double-space combo but we didn't find "
-                    + "\". \" just before the cursor.");
-            return false;
-        }
-        ic.deleteSurroundingText(2, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(2);
-        }
-        ic.commitText("  ", 1);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
-        }
-        return true;
+    // Used by the RingCharBuffer
+    public boolean isWordSeparator(final int code) {
+        return mCurrentSettings.isWordSeparator(code);
     }
 
-    private static boolean revertSwapPunctuation(final InputConnection ic) {
-        // Here we test whether we indeed have a space and something else before us. This should not
-        // be needed, but it's there just in case something went wrong.
-        final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
-        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
-        // enter surrogate pairs this code will have been removed.
-        if (TextUtils.isEmpty(textBeforeCursor)
-                || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
-            // We may only come here if the application is changing the text while we are typing.
-            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
-            // but some debugging log may be in order.
-            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
-                    + "find a space just before the cursor.");
-            return false;
-        }
-        ic.beginBatchEdit();
-        ic.deleteSurroundingText(2, 0);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_deleteSurroundingText(2);
-        }
-        ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_revertSwapPunctuation();
-        }
-        ic.endBatchEdit();
-        return true;
-    }
-
-    public boolean isWordSeparator(int code) {
-        return mSettingsValues.isWordSeparator(code);
-    }
-
-    public boolean preferCapitalization() {
-        return mWordComposer.isFirstCharCapitalized();
-    }
-
-    // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
-    // according to new language or mode.
-    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.
-        if (mKeyboardSwitcher.getKeyboardView() != null) {
-            // Reload keyboard because the current language has been changed.
-            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
-        }
         initSuggest();
-        updateCorrectionMode();
         loadSettings();
+        if (mKeyboardSwitcher.getMainKeyboardView() != null) {
+            // Reload keyboard because the current language has been changed.
+            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
+        }
         // Since we just changed languages, we should re-evaluate suggestions with whatever word
         // we are currently composing. If we are not composing anything, we may want to display
-        // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
-        if (isCursorTouchingWord()) {
-            mHandler.postUpdateSuggestions();
-        } else {
-            mHandler.postUpdateBigramPredictions();
-        }
+        // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway).
+        mHandler.postUpdateSuggestionStrip();
     }
 
     // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
-    // {@link KeyboardSwitcher}.
+    // {@link KeyboardSwitcher}. Called from KeyboardSwitcher
     public void hapticAndAudioFeedback(final int primaryCode) {
-        mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
+        mFeedbackManager.hapticAndAudioFeedback(
+                primaryCode, mKeyboardSwitcher.getMainKeyboardView());
     }
 
+    // Callback called by PointerTracker through the KeyboardActionListener. This is called when a
+    // key is depressed; release matching call is onReleaseKey below.
     @Override
-    public void onPressKey(int primaryCode) {
+    public void onPressKey(final int primaryCode) {
         mKeyboardSwitcher.onPressKey(primaryCode);
     }
 
+    // Callback by PointerTracker through the KeyboardActionListener. This is called when a key
+    // is released; press matching call is onPressKey above.
     @Override
-    public void onReleaseKey(int primaryCode, boolean withSliding) {
+    public void onReleaseKey(final int primaryCode, final boolean withSliding) {
         mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
 
         // If accessibility is on, ensure the user receives keyboard state updates.
@@ -2352,12 +2244,9 @@
             // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
             // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
             // pair-friendly way of deleting characters in InputConnection.
-            final InputConnection ic = getCurrentInputConnection();
-            if (null != ic) {
-                final CharSequence lastChar = ic.getTextBeforeCursor(1, 0);
-                if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
-                    ic.deleteSurroundingText(1, 0);
-                }
+            final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
+            if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
+                mConnection.deleteSurroundingText(1, 0);
             }
         }
     }
@@ -2365,7 +2254,7 @@
     // receive ringer mode change and network state change.
     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
-        public void onReceive(Context context, Intent intent) {
+        public void onReceive(final Context context, final Intent intent) {
             final String action = intent.getAction();
             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
                 mSubtypeSwitcher.onNetworkStateChanged(intent);
@@ -2375,37 +2264,27 @@
         }
     };
 
-    private void updateCorrectionMode() {
-        // TODO: cleanup messy flags
-        final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
-                && !mInputAttributes.mInputTypeNoAutoCorrect;
-        mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
-        mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
-                ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
-    }
-
-    private void updateSuggestionVisibility(final Resources res) {
-        final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
-        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
-            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
-                mSuggestionVisibility = visibility;
-                break;
-            }
-        }
-    }
-
     private void launchSettings() {
-        launchSettingsClass(SettingsActivity.class);
-    }
-
-    public void launchDebugSettings() {
-        launchSettingsClass(DebugSettingsActivity.class);
-    }
-
-    private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
         handleClose();
+        launchSubActivity(SettingsActivity.class);
+    }
+
+    // Called from debug code only
+    public void launchDebugSettings() {
+        handleClose();
+        launchSubActivity(DebugSettingsActivity.class);
+    }
+
+    public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
+        // Put the text in the attached EditText into a safe, saved state before switching to a
+        // new activity that will also use the soft keyboard.
+        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+        launchSubActivity(activityClass);
+    }
+
+    private void launchSubActivity(final Class<? extends Activity> activityClass) {
         Intent intent = new Intent();
-        intent.setClass(LatinIME.this, settingsClass);
+        intent.setClass(LatinIME.this, activityClass);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         startActivity(intent);
     }
@@ -2440,12 +2319,14 @@
         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
                 .setItems(items, listener)
                 .setTitle(title);
-        showOptionDialogInternal(builder.create());
+        showOptionDialog(builder.create());
     }
 
-    private void showOptionDialogInternal(AlertDialog dialog) {
-        final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
-        if (windowToken == null) return;
+    public void showOptionDialog(final AlertDialog dialog) {
+        final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
+        if (windowToken == null) {
+            return;
+        }
 
         dialog.setCancelable(true);
         dialog.setCanceledOnTouchOutside(true);
@@ -2461,8 +2342,19 @@
         dialog.show();
     }
 
+    public void debugDumpStateAndCrashWithException(final String context) {
+        final StringBuilder s = new StringBuilder();
+        s.append("Target application : ").append(mTargetApplicationInfo.name)
+                .append("\nPackage : ").append(mTargetApplicationInfo.packageName)
+                .append("\nTarget app sdk version : ")
+                .append(mTargetApplicationInfo.targetSdkVersion)
+                .append("\nAttributes : ").append(mCurrentSettings.getInputAttributesDebugString())
+                .append("\nContext : ").append(context);
+        throw new RuntimeException(s.toString());
+    }
+
     @Override
-    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
+    protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
         super.dump(fd, fout, args);
 
         final Printer p = new PrintWriterPrinter(fout);
@@ -2470,13 +2362,13 @@
         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
         p.println("  Keyboard mode = " + keyboardMode);
-        p.println("  mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
-        p.println("  mCorrectionMode=" + mCorrectionMode);
+        p.println("  mIsSuggestionsSuggestionsRequested = "
+                + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation));
+        p.println("  mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled);
         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
-        p.println("  mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
-        p.println("  mSoundOn=" + mSettingsValues.mSoundOn);
-        p.println("  mVibrateOn=" + mSettingsValues.mVibrateOn);
-        p.println("  mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
-        p.println("  mInputAttributes=" + mInputAttributes.toString());
+        p.println("  mSoundOn=" + mCurrentSettings.mSoundOn);
+        p.println("  mVibrateOn=" + mCurrentSettings.mVibrateOn);
+        p.println("  mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
+        p.println("  inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index dc0868e..9eab19c 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -44,7 +44,12 @@
             String before, String after, int position, SuggestedWords suggestedWords) {
     }
 
-    public static void logOnAutoCorrection(String before, String after, int separatorCode) {
+    public static void logOnAutoCorrectionForTyping(
+            String before, String after, int separatorCode) {
+    }
+
+    public static void logOnAutoCorrectionForGeometric(String before, String after,
+            int separatorCode, InputPointers inputPointers) {
     }
 
     public static void logOnAutoCorrectionCancelled() {
@@ -71,7 +76,7 @@
     public static void onStartSuggestion(CharSequence previousWords) {
     }
 
-    public static void onAddSuggestedWord(String word, int typeId, int dataType) {
+    public static void onAddSuggestedWord(String word, String sourceDictionaryId) {
     }
 
     public static void onSetKeyboard(Keyboard kb) {
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java
index b938dd3..feb1b2d 100644
--- a/java/src/com/android/inputmethod/latin/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java
@@ -31,7 +31,10 @@
  * update/bugfix to this file, consider also updating/fixing the version in the
  * dictionary pack.
  */
-public class LocaleUtils {
+public final class LocaleUtils {
+    private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap();
+    private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
+
     private LocaleUtils() {
         // Intentional empty constructor for utility class.
     }
@@ -193,7 +196,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.
@@ -219,4 +222,38 @@
             return retval;
         }
     }
+
+    public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
+        if (TextUtils.isEmpty(str)) {
+            return EMPTY_LT_HASH_MAP;
+        }
+        final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER);
+        final int N = ss.length;
+        if (N < 2 || N % 2 != 0) {
+            return EMPTY_LT_HASH_MAP;
+        }
+        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]);
+            retval.put(localeStr, time);
+        }
+        return retval;
+    }
+
+    public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) {
+        if (map == null || map.isEmpty()) {
+            return "";
+        }
+        final StringBuilder builder = new StringBuilder();
+        for (String localeStr : map.keySet()) {
+            if (builder.length() > 0) {
+                builder.append(LOCALE_AND_TIME_STR_SEPARATER);
+            }
+            final Long time = map.get(localeStr);
+            builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER);
+            builder.append(String.valueOf(time));
+        }
+        return builder.toString();
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
deleted file mode 100644
index 66d6d58..0000000
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ /dev/null
@@ -1,757 +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;
-
-import android.content.SharedPreferences;
-import android.inputmethodservice.InputMethodService;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.os.SystemClock;
-import android.preference.PreferenceManager;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.inputmethod.CompletionInfo;
-import android.view.inputmethod.EditorInfo;
-
-import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.internal.KeyboardState;
-import com.android.inputmethod.latin.define.ProductionFlag;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.charset.Charset;
-import java.util.Map;
-
-/**
- * Logs the use of the LatinIME keyboard.
- *
- * This class logs operations on the IME keyboard, including what the user has typed.
- * Data is stored locally in a file in app-specific storage.
- *
- * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
- */
-public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
-    private static final String TAG = ResearchLogger.class.getSimpleName();
-    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
-    private static final boolean DEBUG = false;
-
-    private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager());
-    public static boolean sIsLogging = false;
-    /* package */ final Handler mLoggingHandler;
-    private InputMethodService mIms;
-
-    /**
-     * Isolates management of files. This variable should never be null, but can be changed
-     * to support testing.
-     */
-    /* package */ LogFileManager mLogFileManager;
-
-    /**
-     * Manages the file(s) that stores the logs.
-     *
-     * Handles creation, deletion, and provides Readers, Writers, and InputStreams to access
-     * the logs.
-     */
-    /* package */ static class LogFileManager {
-        public static final String RESEARCH_LOG_FILENAME_KEY = "RESEARCH_LOG_FILENAME";
-
-        private static final String DEFAULT_FILENAME = "researchLog.txt";
-        private static final long LOGFILE_PURGE_INTERVAL = 1000 * 60 * 60 * 24;
-
-        protected InputMethodService mIms;
-        protected File mFile;
-        protected PrintWriter mPrintWriter;
-
-        /* package */ LogFileManager() {
-        }
-
-        public void init(final InputMethodService ims) {
-            mIms = ims;
-        }
-
-        public synchronized void createLogFile() throws IOException {
-            createLogFile(DEFAULT_FILENAME);
-        }
-
-        public synchronized void createLogFile(final SharedPreferences prefs)
-                throws IOException {
-            final String filename =
-                    prefs.getString(RESEARCH_LOG_FILENAME_KEY, DEFAULT_FILENAME);
-            createLogFile(filename);
-        }
-
-        public synchronized void createLogFile(final String filename)
-                throws IOException {
-            if (mIms == null) {
-                final String msg = "InputMethodService is not configured.  Logging is off.";
-                Log.w(TAG, msg);
-                throw new IOException(msg);
-            }
-            final File filesDir = mIms.getFilesDir();
-            if (filesDir == null || !filesDir.exists()) {
-                final String msg = "Storage directory does not exist.  Logging is off.";
-                Log.w(TAG, msg);
-                throw new IOException(msg);
-            }
-            close();
-            final File file = new File(filesDir, filename);
-            mFile = file;
-            boolean append = true;
-            if (file.exists() && file.lastModified() + LOGFILE_PURGE_INTERVAL <
-                    System.currentTimeMillis()) {
-                append = false;
-            }
-            mPrintWriter = new PrintWriter(new BufferedWriter(new FileWriter(file, append)), true);
-        }
-
-        public synchronized boolean append(final String s) {
-            PrintWriter printWriter = mPrintWriter;
-            if (printWriter == null || !mFile.exists()) {
-                if (DEBUG) {
-                    Log.w(TAG, "PrintWriter is null... attempting to create default log file");
-                }
-                try {
-                    createLogFile();
-                    printWriter = mPrintWriter;
-                } catch (IOException e) {
-                    Log.w(TAG, "Failed to create log file.  Not logging.");
-                    return false;
-                }
-            }
-            printWriter.print(s);
-            printWriter.flush();
-            return !printWriter.checkError();
-        }
-
-        public synchronized void reset() {
-            if (mPrintWriter != null) {
-                mPrintWriter.close();
-                mPrintWriter = null;
-                if (DEBUG) {
-                    Log.d(TAG, "logfile closed");
-                }
-            }
-            if (mFile != null) {
-                mFile.delete();
-                if (DEBUG) {
-                    Log.d(TAG, "logfile deleted");
-                }
-                mFile = null;
-            }
-        }
-
-        public synchronized void close() {
-            if (mPrintWriter != null) {
-                mPrintWriter.close();
-                mPrintWriter = null;
-                mFile = null;
-                if (DEBUG) {
-                    Log.d(TAG, "logfile closed");
-                }
-            }
-        }
-
-        /* package */ synchronized void flush() {
-            if (mPrintWriter != null) {
-                mPrintWriter.flush();
-            }
-        }
-
-        /* package */ synchronized String getContents() {
-            final File file = mFile;
-            if (file == null) {
-                return "";
-            }
-            if (mPrintWriter != null) {
-                mPrintWriter.flush();
-            }
-            FileInputStream stream = null;
-            FileChannel fileChannel = null;
-            String s = "";
-            try {
-                stream = new FileInputStream(file);
-                fileChannel = stream.getChannel();
-                final ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
-                fileChannel.read(byteBuffer);
-                byteBuffer.rewind();
-                CharBuffer charBuffer = Charset.defaultCharset().decode(byteBuffer);
-                s = charBuffer.toString();
-            } catch (IOException e) {
-                e.printStackTrace();
-            } finally {
-                try {
-                    if (fileChannel != null) {
-                        fileChannel.close();
-                    }
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } finally {
-                    try {
-                        if (stream != null) {
-                            stream.close();
-                        }
-                    } catch (IOException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-            return s;
-        }
-    }
-
-    private ResearchLogger(final LogFileManager logFileManager) {
-        final HandlerThread handlerThread = new HandlerThread("ResearchLogger logging task",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        handlerThread.start();
-        mLoggingHandler = new Handler(handlerThread.getLooper());
-        mLogFileManager = logFileManager;
-    }
-
-    public static ResearchLogger getInstance() {
-        return sInstance;
-    }
-
-    public static void init(final InputMethodService ims, final SharedPreferences prefs) {
-        sInstance.initInternal(ims, prefs);
-    }
-
-    /* package */ void initInternal(final InputMethodService ims, final SharedPreferences prefs) {
-        mIms = ims;
-        final LogFileManager logFileManager = mLogFileManager;
-        if (logFileManager != null) {
-            logFileManager.init(ims);
-            try {
-                logFileManager.createLogFile(prefs);
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        if (prefs != null) {
-            sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
-            prefs.registerOnSharedPreferenceChangeListener(this);
-        }
-    }
-
-    /**
-     * Represents a category of logging events that share the same subfield structure.
-     */
-    private static enum LogGroup {
-        MOTION_EVENT("m"),
-        KEY("k"),
-        CORRECTION("c"),
-        STATE_CHANGE("s"),
-        UNSTRUCTURED("u");
-
-        private final String mLogString;
-
-        private LogGroup(final String logString) {
-            mLogString = logString;
-        }
-    }
-
-    public void logMotionEvent(final int action, final long eventTime, final int id,
-            final int x, final int y, final float size, final float pressure) {
-        final String eventTag;
-        switch (action) {
-            case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break;
-            case MotionEvent.ACTION_UP: eventTag = "[Up]"; break;
-            case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break;
-            case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break;
-            case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break;
-            case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break;
-            case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break;
-            default: eventTag = "[Action" + action + "]"; break;
-        }
-        if (!TextUtils.isEmpty(eventTag)) {
-            final StringBuilder sb = new StringBuilder();
-            sb.append(eventTag);
-            sb.append('\t'); sb.append(eventTime);
-            sb.append('\t'); sb.append(id);
-            sb.append('\t'); sb.append(x);
-            sb.append('\t'); sb.append(y);
-            sb.append('\t'); sb.append(size);
-            sb.append('\t'); sb.append(pressure);
-            write(LogGroup.MOTION_EVENT, sb.toString());
-        }
-    }
-
-    public void logKeyEvent(final int code, final int x, final int y) {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(Keyboard.printableCode(code));
-        sb.append('\t'); sb.append(x);
-        sb.append('\t'); sb.append(y);
-        write(LogGroup.KEY, sb.toString());
-    }
-
-    public void logCorrection(final String subgroup, final String before, final String after,
-            final int position) {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(subgroup);
-        sb.append('\t'); sb.append(before);
-        sb.append('\t'); sb.append(after);
-        sb.append('\t'); sb.append(position);
-        write(LogGroup.CORRECTION, sb.toString());
-    }
-
-    public void logStateChange(final String subgroup, final String details) {
-        write(LogGroup.STATE_CHANGE, subgroup + "\t" + details);
-    }
-
-    public static class UnsLogGroup {
-        private static final boolean DEFAULT_ENABLED = true;
-
-        private static final boolean KEYBOARDSTATE_ONCANCELINPUT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONCODEINPUT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONPRESSKEY_ENABLED = DEFAULT_ENABLED;
-        private static final boolean KEYBOARDSTATE_ONRELEASEKEY_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_COMMITTEXT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_DELETESURROUNDINGTEXT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_ONDISPLAYCOMPLETIONS_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_ONUPDATESELECTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PERFORMEDITORACTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_PICKSUGGESTIONMANUALLY_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_REVERTCOMMIT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINIME_REVERTSWAPPUNCTUATION_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_SENDKEYCODEPOINT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED = DEFAULT_ENABLED;
-        private static final boolean LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean
-                POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_ONDOWNEVENT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean POINTERTRACKER_ONMOVEEVENT_ENABLED = DEFAULT_ENABLED;
-        private static final boolean SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED
-                = DEFAULT_ENABLED;
-        private static final boolean SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED = DEFAULT_ENABLED;
-    }
-
-    public static void logUnstructured(String logGroup, final String details) {
-        // TODO: improve performance by making entire class static and/or implementing natively
-        getInstance().write(LogGroup.UNSTRUCTURED, logGroup + "\t" + details);
-    }
-
-    private void write(final LogGroup logGroup, final String log) {
-        // TODO: rewrite in native for better performance
-        mLoggingHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                final long currentTime = System.currentTimeMillis();
-                final long upTime = SystemClock.uptimeMillis();
-                final StringBuilder builder = new StringBuilder();
-                builder.append(currentTime);
-                builder.append('\t'); builder.append(upTime);
-                builder.append('\t'); builder.append(logGroup.mLogString);
-                builder.append('\t'); builder.append(log);
-                builder.append('\n');
-                if (DEBUG) {
-                    Log.d(TAG, "Write: " + '[' + logGroup.mLogString + ']' + log);
-                }
-                final String s = builder.toString();
-                if (mLogFileManager.append(s)) {
-                    // success
-                } else {
-                    if (DEBUG) {
-                        Log.w(TAG, "Unable to write to log.");
-                    }
-                    // perhaps logfile was deleted.  try to recreate and relog.
-                    try {
-                        mLogFileManager.createLogFile(PreferenceManager
-                                .getDefaultSharedPreferences(mIms));
-                        mLogFileManager.append(s);
-                    } catch (IOException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        });
-    }
-
-    public void clearAll() {
-        mLoggingHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (DEBUG) {
-                    Log.d(TAG, "Delete log file.");
-                }
-                mLogFileManager.reset();
-            }
-        });
-    }
-
-    /* package */ LogFileManager getLogFileManager() {
-        return mLogFileManager;
-    }
-
-    @Override
-    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
-        if (key == null || prefs == null) {
-            return;
-        }
-        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
-    }
-
-    public static void keyboardState_onCancelInput(final boolean isSinglePointer,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONCANCELINPUT_ENABLED) {
-            final String s = "onCancelInput: single=" + isSinglePointer + " " + keyboardState;
-            logUnstructured("KeyboardState_onCancelInput", s);
-        }
-    }
-
-    public static void keyboardState_onCodeInput(
-            final int code, final boolean isSinglePointer, final int autoCaps,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONCODEINPUT_ENABLED) {
-            final String s = "onCodeInput: code=" + Keyboard.printableCode(code)
-                    + " single=" + isSinglePointer
-                    + " autoCaps=" + autoCaps + " " + keyboardState;
-            logUnstructured("KeyboardState_onCodeInput", s);
-        }
-    }
-
-    public static void keyboardState_onLongPressTimeout(final int code,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED) {
-            final String s = "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " "
-                    + keyboardState;
-            logUnstructured("KeyboardState_onLongPressTimeout", s);
-        }
-    }
-
-    public static void keyboardState_onPressKey(final int code,
-            final KeyboardState keyboardState) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONPRESSKEY_ENABLED) {
-            final String s = "onPressKey: code=" + Keyboard.printableCode(code) + " "
-                    + keyboardState;
-            logUnstructured("KeyboardState_onPressKey", s);
-        }
-    }
-
-    public static void keyboardState_onReleaseKey(final KeyboardState keyboardState, final int code,
-            final boolean withSliding) {
-        if (UnsLogGroup.KEYBOARDSTATE_ONRELEASEKEY_ENABLED) {
-            final String s = "onReleaseKey: code=" + Keyboard.printableCode(code)
-                    + " sliding=" + withSliding + " " + keyboardState;
-            logUnstructured("KeyboardState_onReleaseKey", s);
-        }
-    }
-
-    public static void latinIME_commitCurrentAutoCorrection(final String typedWord,
-            final String autoCorrection) {
-        if (UnsLogGroup.LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED) {
-            if (typedWord.equals(autoCorrection)) {
-                getInstance().logCorrection("[----]", typedWord, autoCorrection, -1);
-            } else {
-                getInstance().logCorrection("[Auto]", typedWord, autoCorrection, -1);
-            }
-        }
-    }
-
-    public static void latinIME_commitText(final CharSequence typedWord) {
-        if (UnsLogGroup.LATINIME_COMMITTEXT_ENABLED) {
-            logUnstructured("LatinIME_commitText", typedWord.toString());
-        }
-    }
-
-    public static void latinIME_deleteSurroundingText(final int length) {
-        if (UnsLogGroup.LATINIME_DELETESURROUNDINGTEXT_ENABLED) {
-            logUnstructured("LatinIME_deleteSurroundingText", String.valueOf(length));
-        }
-    }
-
-    public static void latinIME_doubleSpaceAutoPeriod() {
-        if (UnsLogGroup.LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED) {
-            logUnstructured("LatinIME_doubleSpaceAutoPeriod", "");
-        }
-    }
-
-    public static void latinIME_onDisplayCompletions(
-            final CompletionInfo[] applicationSpecifiedCompletions) {
-        if (UnsLogGroup.LATINIME_ONDISPLAYCOMPLETIONS_ENABLED) {
-            final StringBuilder builder = new StringBuilder();
-            builder.append("Received completions:");
-            if (applicationSpecifiedCompletions != null) {
-                for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
-                    builder.append("  #");
-                    builder.append(i);
-                    builder.append(": ");
-                    builder.append(applicationSpecifiedCompletions[i]);
-                    builder.append("\n");
-                }
-            }
-            logUnstructured("LatinIME_onDisplayCompletions", builder.toString());
-        }
-    }
-
-    public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
-            final SharedPreferences prefs) {
-        if (UnsLogGroup.LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED) {
-            final StringBuilder builder = new StringBuilder();
-            builder.append("onStartInputView: editorInfo:");
-            builder.append("\tinputType=");
-            builder.append(Integer.toHexString(editorInfo.inputType));
-            builder.append("\timeOptions=");
-            builder.append(Integer.toHexString(editorInfo.imeOptions));
-            builder.append("\tdisplay="); builder.append(Build.DISPLAY);
-            builder.append("\tmodel="); builder.append(Build.MODEL);
-            for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
-                builder.append("\t" + entry.getKey());
-                Object value = entry.getValue();
-                builder.append("=" + ((value == null) ? "<null>" : value.toString()));
-            }
-            logUnstructured("LatinIME_onStartInputViewInternal", builder.toString());
-        }
-    }
-
-    public static void latinIME_onUpdateSelection(final int lastSelectionStart,
-            final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
-            final int newSelStart, final int newSelEnd, final int composingSpanStart,
-            final int composingSpanEnd) {
-        if (UnsLogGroup.LATINIME_ONUPDATESELECTION_ENABLED) {
-            final String s = "onUpdateSelection: oss=" + oldSelStart
-                    + ", ose=" + oldSelEnd
-                    + ", lss=" + lastSelectionStart
-                    + ", lse=" + lastSelectionEnd
-                    + ", nss=" + newSelStart
-                    + ", nse=" + newSelEnd
-                    + ", cs=" + composingSpanStart
-                    + ", ce=" + composingSpanEnd;
-            logUnstructured("LatinIME_onUpdateSelection", s);
-        }
-    }
-
-    public static void latinIME_performEditorAction(final int imeActionNext) {
-        if (UnsLogGroup.LATINIME_PERFORMEDITORACTION_ENABLED) {
-            logUnstructured("LatinIME_performEditorAction", String.valueOf(imeActionNext));
-        }
-    }
-
-    public static void latinIME_pickApplicationSpecifiedCompletion(final int index,
-            final CharSequence text, int x, int y) {
-        if (UnsLogGroup.LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED) {
-            final String s = String.valueOf(index) + '\t' + text + '\t' + x + '\t' + y;
-            logUnstructured("LatinIME_pickApplicationSpecifiedCompletion", s);
-        }
-    }
-
-    public static void latinIME_pickSuggestionManually(final String replacedWord,
-            final int index, CharSequence suggestion, int x, int y) {
-        if (UnsLogGroup.LATINIME_PICKSUGGESTIONMANUALLY_ENABLED) {
-            final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
-            logUnstructured("LatinIME_pickSuggestionManually", s);
-        }
-    }
-
-    public static void latinIME_punctuationSuggestion(final int index,
-            final CharSequence suggestion, int x, int y) {
-        if (UnsLogGroup.LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED) {
-            final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
-            logUnstructured("LatinIME_pickPunctuationSuggestion", s);
-        }
-    }
-
-    public static void latinIME_revertDoubleSpaceWhileInBatchEdit() {
-        if (UnsLogGroup.LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED) {
-            logUnstructured("LatinIME_revertDoubleSpaceWhileInBatchEdit", "");
-        }
-    }
-
-    public static void latinIME_revertSwapPunctuation() {
-        if (UnsLogGroup.LATINIME_REVERTSWAPPUNCTUATION_ENABLED) {
-            logUnstructured("LatinIME_revertSwapPunctuation", "");
-        }
-    }
-
-    public static void latinIME_sendKeyCodePoint(final int code) {
-        if (UnsLogGroup.LATINIME_SENDKEYCODEPOINT_ENABLED) {
-            logUnstructured("LatinIME_sendKeyCodePoint", String.valueOf(code));
-        }
-    }
-
-    public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() {
-        if (UnsLogGroup.LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED) {
-            logUnstructured("latinIME_swapSwapperAndSpaceWhileInBatchEdit", "");
-        }
-    }
-
-    public static void latinIME_switchToKeyboardView() {
-        if (UnsLogGroup.LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED) {
-            final String s = "Switch to keyboard view.";
-            logUnstructured("LatinIME_switchToKeyboardView", s);
-        }
-    }
-
-    public static void latinKeyboardView_onLongPress() {
-        if (UnsLogGroup.LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED) {
-            final String s = "long press detected";
-            logUnstructured("LatinKeyboardView_onLongPress", s);
-        }
-    }
-
-    public static void latinKeyboardView_processMotionEvent(MotionEvent me, int action,
-            long eventTime, int index, int id, int x, int y) {
-        if (UnsLogGroup.LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED) {
-            final float size = me.getSize(index);
-            final float pressure = me.getPressure(index);
-            if (action != MotionEvent.ACTION_MOVE) {
-                getInstance().logMotionEvent(action, eventTime, id, x, y, size, pressure);
-            }
-        }
-    }
-
-    public static void latinKeyboardView_setKeyboard(final Keyboard keyboard) {
-        if (UnsLogGroup.LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED) {
-            StringBuilder builder = new StringBuilder();
-            builder.append("id=");
-            builder.append(keyboard.mId);
-            builder.append("\tw=");
-            builder.append(keyboard.mOccupiedWidth);
-            builder.append("\th=");
-            builder.append(keyboard.mOccupiedHeight);
-            builder.append("\tkeys=[");
-            boolean first = true;
-            for (Key key : keyboard.mKeys) {
-                if (first) {
-                    first = false;
-                } else {
-                    builder.append(",");
-                }
-                builder.append("{code:");
-                builder.append(key.mCode);
-                builder.append(",altCode:");
-                builder.append(key.mAltCode);
-                builder.append(",x:");
-                builder.append(key.mX);
-                builder.append(",y:");
-                builder.append(key.mY);
-                builder.append(",w:");
-                builder.append(key.mWidth);
-                builder.append(",h:");
-                builder.append(key.mHeight);
-                builder.append("}");
-            }
-            builder.append("]");
-            logUnstructured("LatinKeyboardView_setKeyboard", builder.toString());
-        }
-    }
-
-    public static void latinIME_revertCommit(final String originallyTypedWord) {
-        if (UnsLogGroup.LATINIME_REVERTCOMMIT_ENABLED) {
-            logUnstructured("LatinIME_revertCommit", originallyTypedWord);
-        }
-    }
-
-    public static void pointerTracker_callListenerOnCancelInput() {
-        final String s = "onCancelInput";
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED) {
-            logUnstructured("PointerTracker_callListenerOnCancelInput", s);
-        }
-    }
-
-    public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
-            final int y, final boolean ignoreModifierKey, final boolean altersCode,
-            final int code) {
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED) {
-            final String s = "onCodeInput: " + Keyboard.printableCode(code)
-                    + " text=" + key.mOutputText + " x=" + x + " y=" + y
-                    + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
-                    + " enabled=" + key.isEnabled();
-            logUnstructured("PointerTracker_callListenerOnCodeInput", s);
-        }
-    }
-
-    public static void pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(
-            final Key key, final boolean ignoreModifierKey) {
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED) {
-            final String s = "onPress    : " + KeyDetector.printableCode(key)
-                    + " ignoreModifier=" + ignoreModifierKey
-                    + " enabled=" + key.isEnabled();
-            logUnstructured("PointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange", s);
-        }
-    }
-
-    public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
-            final boolean withSliding, final boolean ignoreModifierKey) {
-        if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED) {
-            final String s = "onRelease  : " + Keyboard.printableCode(primaryCode)
-                    + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey
-                    + " enabled="+ key.isEnabled();
-            logUnstructured("PointerTracker_callListenerOnRelease", s);
-        }
-    }
-
-    public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
-        if (UnsLogGroup.POINTERTRACKER_ONDOWNEVENT_ENABLED) {
-            final String s = "onDownEvent: ignore potential noise: time=" + deltaT
-                    + " distance=" + distanceSquared;
-            logUnstructured("PointerTracker_onDownEvent", s);
-        }
-    }
-
-    public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
-            final int lastY) {
-        if (UnsLogGroup.POINTERTRACKER_ONMOVEEVENT_ENABLED) {
-            final String s = String.format("onMoveEvent: sudden move is translated to "
-                    + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y);
-            logUnstructured("PointerTracker_onMoveEvent", s);
-        }
-    }
-
-    public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
-        if (UnsLogGroup.SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED) {
-            final String s = "onTouchEvent: ignore sudden jump " + me;
-            logUnstructured("SuddenJumpingTouchEventHandler_onTouchEvent", s);
-        }
-    }
-
-    public static void suggestionsView_setSuggestions(final SuggestedWords mSuggestedWords) {
-        if (UnsLogGroup.SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED) {
-            logUnstructured("SuggestionsView_setSuggestions", mSuggestedWords.toString());
-        }
-    }
-}
\ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/ResizableIntArray.java
new file mode 100644
index 0000000..c660f92
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ResizableIntArray.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.latin;
+
+import java.util.Arrays;
+
+// TODO: This class is not thread-safe.
+public class ResizableIntArray {
+    private int[] mArray;
+    private int mLength;
+
+    public ResizableIntArray(final int capacity) {
+        reset(capacity);
+    }
+
+    public int get(final int index) {
+        if (index < mLength) {
+            return mArray[index];
+        }
+        throw new ArrayIndexOutOfBoundsException("length=" + mLength + "; index=" + index);
+    }
+
+    public void add(final int index, final int val) {
+        if (index < mLength) {
+            mArray[index] = val;
+        } else {
+            mLength = index;
+            add(val);
+        }
+    }
+
+    public void add(final int val) {
+        final int currentLength = mLength;
+        ensureCapacity(currentLength + 1);
+        mArray[currentLength] = val;
+        mLength = currentLength + 1;
+    }
+
+    /**
+     * Calculate the new capacity of {@code mArray}.
+     * @param minimumCapacity the minimum capacity that the {@code mArray} should have.
+     * @return the new capacity that the {@code mArray} should have. Returns zero when there is no
+     * need to expand {@code mArray}.
+     */
+    private int calculateCapacity(final int minimumCapacity) {
+        final int currentCapcity = mArray.length;
+        if (currentCapcity < minimumCapacity) {
+            final int nextCapacity = currentCapcity * 2;
+            // The following is the same as return Math.max(minimumCapacity, nextCapacity);
+            return minimumCapacity > nextCapacity ? minimumCapacity : nextCapacity;
+        }
+        return 0;
+    }
+
+    private void ensureCapacity(final int minimumCapacity) {
+        final int newCapacity = calculateCapacity(minimumCapacity);
+        if (newCapacity > 0) {
+            // TODO: Implement primitive array pool.
+            mArray = Arrays.copyOf(mArray, newCapacity);
+        }
+    }
+
+    public int getLength() {
+        return mLength;
+    }
+
+    public void setLength(final int newLength) {
+        ensureCapacity(newLength);
+        mLength = newLength;
+    }
+
+    public void reset(final int capacity) {
+        // TODO: Implement primitive array pool.
+        mArray = new int[capacity];
+        mLength = 0;
+    }
+
+    public int[] getPrimitiveArray() {
+        return mArray;
+    }
+
+    public void set(final ResizableIntArray ip) {
+        // TODO: Implement primitive array pool.
+        mArray = ip.mArray;
+        mLength = ip.mLength;
+    }
+
+    public void copy(final ResizableIntArray ip) {
+        final int newCapacity = calculateCapacity(ip.mLength);
+        if (newCapacity > 0) {
+            // TODO: Implement primitive array pool.
+            mArray = new int[newCapacity];
+        }
+        System.arraycopy(ip.mArray, 0, mArray, 0, ip.mLength);
+        mLength = ip.mLength;
+    }
+
+    public void append(final ResizableIntArray src, final int startPos, final int length) {
+        if (length == 0) {
+            return;
+        }
+        final int currentLength = mLength;
+        final int newLength = currentLength + length;
+        ensureCapacity(newLength);
+        System.arraycopy(src.mArray, startPos, mArray, currentLength, length);
+        mLength = newLength;
+    }
+
+    public void fill(final int value, final int startPos, final int length) {
+        if (startPos < 0 || length < 0) {
+            throw new IllegalArgumentException("startPos=" + startPos + "; length=" + length);
+        }
+        final int endPos = startPos + length;
+        ensureCapacity(endPos);
+        Arrays.fill(mArray, startPos, endPos, value);
+        if (mLength < endPos) {
+            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/ResourceUtils.java b/java/src/com/android/inputmethod/latin/ResourceUtils.java
new file mode 100644
index 0000000..5021ad3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ResourceUtils.java
@@ -0,0 +1,128 @@
+/*
+ * 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.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.util.TypedValue;
+
+import java.util.HashMap;
+
+public final class ResourceUtils {
+    public static final float UNDEFINED_RATIO = -1.0f;
+    public static final int UNDEFINED_DIMENSION = -1;
+
+    private ResourceUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
+    private static final HashMap<String, String> sDeviceOverrideValueMap =
+            CollectionUtils.newHashMap();
+
+    public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
+        final int orientation = res.getConfiguration().orientation;
+        final String key = overrideResId + "-" + orientation;
+        if (!sDeviceOverrideValueMap.containsKey(key)) {
+            String overrideValue = defValue;
+            for (final String element : res.getStringArray(overrideResId)) {
+                if (element.startsWith(HARDWARE_PREFIX)) {
+                    overrideValue = element.substring(HARDWARE_PREFIX.length());
+                    break;
+                }
+            }
+            sDeviceOverrideValueMap.put(key, overrideValue);
+        }
+        return sDeviceOverrideValueMap.get(key);
+    }
+
+    public static boolean isValidFraction(final float fraction) {
+        return fraction >= 0.0f;
+    }
+
+    // {@link Resources#getDimensionPixelSize(int)} returns at least one pixel size.
+    public static boolean isValidDimensionPixelSize(final int dimension) {
+        return dimension > 0;
+    }
+
+    // {@link Resources#getDimensionPixelOffset(int)} may return zero pixel offset.
+    public static boolean isValidDimensionPixelOffset(final int dimension) {
+        return dimension >= 0;
+    }
+
+    public static float getFraction(final TypedArray a, final int index, final float defValue) {
+        final TypedValue value = a.peekValue(index);
+        if (value == null || !isFractionValue(value)) {
+            return defValue;
+        }
+        return a.getFraction(index, 1, 1, defValue);
+    }
+
+    public static float getFraction(final TypedArray a, final int index) {
+        return getFraction(a, index, UNDEFINED_RATIO);
+    }
+
+    public static int getDimensionPixelSize(final TypedArray a, final int index) {
+        final TypedValue value = a.peekValue(index);
+        if (value == null || !isDimensionValue(value)) {
+            return ResourceUtils.UNDEFINED_DIMENSION;
+        }
+        return a.getDimensionPixelSize(index, ResourceUtils.UNDEFINED_DIMENSION);
+    }
+
+    public static float getDimensionOrFraction(TypedArray a, int index, int base,
+            float defValue) {
+        final TypedValue value = a.peekValue(index);
+        if (value == null) {
+            return defValue;
+        }
+        if (isFractionValue(value)) {
+            return a.getFraction(index, base, base, defValue);
+        } else if (isDimensionValue(value)) {
+            return a.getDimension(index, defValue);
+        }
+        return defValue;
+    }
+
+    public static int getEnumValue(TypedArray a, int index, int defValue) {
+        final TypedValue value = a.peekValue(index);
+        if (value == null) {
+            return defValue;
+        }
+        if (isIntegerValue(value)) {
+            return a.getInt(index, defValue);
+        }
+        return defValue;
+    }
+
+    public static boolean isFractionValue(TypedValue v) {
+        return v.type == TypedValue.TYPE_FRACTION;
+    }
+
+    public static boolean isDimensionValue(TypedValue v) {
+        return v.type == TypedValue.TYPE_DIMENSION;
+    }
+
+    public static boolean isIntegerValue(TypedValue v) {
+        return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
+    }
+
+    public static boolean isStringValue(TypedValue v) {
+        return v.type == TypedValue.TYPE_STRING;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
new file mode 100644
index 0000000..28c0c0f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -0,0 +1,701 @@
+/*
+ * 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.inputmethodservice.InputMethodService;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Enrichment class for InputConnection to simplify interaction and add functionality.
+ *
+ * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
+ * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
+ * all the time to find out what text is in the buffer, when we need it to determine caps mode
+ * for example.
+ */
+public class RichInputConnection {
+    private static final String TAG = RichInputConnection.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean DEBUG_PREVIOUS_TEXT = false;
+    // Provision for a long word pair and a separator
+    private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
+    private static final Pattern spaceRegex = Pattern.compile("\\s+");
+    private static final int INVALID_CURSOR_POSITION = -1;
+
+    /**
+     * This variable contains the value LatinIME thinks the cursor position should be at now.
+     * This is a few steps in advance of what the TextView thinks it is, because TextView will
+     * only know after the IPC calls gets through.
+     */
+    private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points
+    /**
+     * This contains the committed text immediately preceding the cursor and the composing
+     * text if any. It is refreshed when the cursor moves by calling upon the TextView.
+     */
+    private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
+    /**
+     * This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
+     */
+    private StringBuilder mComposingText = new StringBuilder();
+    /**
+     * This is a one-character string containing the character after the cursor. Since LatinIME
+     * never touches it directly, it's never modified by any means other than re-reading from the
+     * TextView when the cursor position is changed by the user.
+     */
+    private CharSequence mCharAfterTheCursor = "";
+    // A hint on how many characters to cache from the TextView. A good value of this is given by
+    // how many characters we need to be able to almost always find the caps mode.
+    private static final int DEFAULT_TEXT_CACHE_SIZE = 100;
+
+    private final InputMethodService mParent;
+    InputConnection mIC;
+    int mNestLevel;
+    public RichInputConnection(final InputMethodService parent) {
+        mParent = parent;
+        mIC = null;
+        mNestLevel = 0;
+    }
+
+    private void checkConsistencyForDebug() {
+        final ExtractedTextRequest r = new ExtractedTextRequest();
+        r.hintMaxChars = 0;
+        r.hintMaxLines = 0;
+        r.token = 1;
+        r.flags = 0;
+        final ExtractedText et = mIC.getExtractedText(r, 0);
+        final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
+        final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
+                .append(mComposingText);
+        if (null == et || null == beforeCursor) return;
+        final int actualLength = Math.min(beforeCursor.length(), internal.length());
+        if (internal.length() > actualLength) {
+            internal.delete(0, internal.length() - actualLength);
+        }
+        final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
+                : beforeCursor.subSequence(beforeCursor.length() - actualLength,
+                        beforeCursor.length()).toString();
+        if (et.selectionStart != mCurrentCursorPosition
+                || !(reference.equals(internal.toString()))) {
+            final String context = "Expected cursor position = " + mCurrentCursorPosition
+                    + "\nActual cursor position = " + et.selectionStart
+                    + "\nExpected text = " + internal.length() + " " + internal
+                    + "\nActual text = " + reference.length() + " " + reference;
+            ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
+        } else {
+            Log.e(TAG, Utils.getStackTrace(2));
+            Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart);
+        }
+    }
+
+    public void beginBatchEdit() {
+        if (++mNestLevel == 1) {
+            mIC = mParent.getCurrentInputConnection();
+            if (null != mIC) {
+                mIC.beginBatchEdit();
+            }
+        } else {
+            if (DBG) {
+                throw new RuntimeException("Nest level too deep");
+            } else {
+                Log.e(TAG, "Nest level too deep : " + mNestLevel);
+            }
+        }
+        checkBatchEdit();
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+    }
+
+    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 (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+    }
+
+    public void resetCachesUponCursorMove(final int newCursorPosition) {
+        mCurrentCursorPosition = newCursorPosition;
+        mComposingText.setLength(0);
+        mCommittedTextBeforeComposingText.setLength(0);
+        mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
+        mCharAfterTheCursor = getTextAfterCursor(1, 0);
+        if (null != mIC) {
+            mIC.finishComposingText();
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_finishComposingText();
+            }
+        }
+    }
+
+    private void checkBatchEdit() {
+        if (mNestLevel != 1) {
+            // TODO: exception instead
+            Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
+            Log.e(TAG, Utils.getStackTrace(4));
+        }
+    }
+
+    public void finishComposingText() {
+        checkBatchEdit();
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+        mCommittedTextBeforeComposingText.append(mComposingText);
+        mCurrentCursorPosition += mComposingText.length();
+        mComposingText.setLength(0);
+        if (null != mIC) {
+            mIC.finishComposingText();
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_finishComposingText();
+            }
+        }
+    }
+
+    public void commitText(final CharSequence text, final int i) {
+        checkBatchEdit();
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+        mCommittedTextBeforeComposingText.append(text);
+        mCurrentCursorPosition += text.length() - mComposingText.length();
+        mComposingText.setLength(0);
+        if (null != mIC) {
+            mIC.commitText(text, i);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_commitText(text, i);
+            }
+        }
+    }
+
+    /**
+     * Gets the caps modes we should be in after this specific string.
+     *
+     * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument.
+     * This method also supports faking an additional space after the string passed in argument,
+     * to support cases where a space will be added automatically, like in phantom space
+     * state for example.
+     * Note that for English, we are using American typography rules (which are not specific to
+     * American English, it's just the most common set of rules for English).
+     *
+     * @param inputType a mask of the caps modes to test for.
+     * @param locale what language should be considered.
+     * @param hasSpaceBefore if we should consider there should be a space after the string.
+     * @return the caps modes that should be on as a set of bits
+     */
+    public int getCursorCapsMode(final int inputType, final Locale locale,
+            final boolean hasSpaceBefore) {
+        mIC = mParent.getCurrentInputConnection();
+        if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
+        if (!TextUtils.isEmpty(mComposingText)) {
+            if (hasSpaceBefore) {
+                // If we have some composing text and a space before, then we should have
+                // MODE_CHARACTERS and MODE_WORDS on.
+                return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
+            } else {
+                // We have some composing text - we should be in MODE_CHARACTERS only.
+                return TextUtils.CAP_MODE_CHARACTERS & inputType;
+            }
+        }
+        // TODO: this will generally work, but there may be cases where the buffer contains SOME
+        // information but not enough to determine the caps mode accurately. This may happen after
+        // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so.
+        // getCapsMode should be updated to be able to return a "not enough info" result so that
+        // we can get more context only when needed.
+        if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mCurrentCursorPosition) {
+            mCommittedTextBeforeComposingText.append(
+                    getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
+        }
+        // This never calls InputConnection#getCapsMode - in fact, it's a static method that
+        // never blocks or initiates IPC.
+        return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale,
+                hasSpaceBefore);
+    }
+
+    public CharSequence getTextBeforeCursor(final int i, final int j) {
+        mIC = mParent.getCurrentInputConnection();
+        if (null != mIC) return mIC.getTextBeforeCursor(i, j);
+        return null;
+    }
+
+    public CharSequence getTextAfterCursor(final int i, final int j) {
+        mIC = mParent.getCurrentInputConnection();
+        if (null != mIC) return mIC.getTextAfterCursor(i, j);
+        return null;
+    }
+
+    public void deleteSurroundingText(final int i, final int j) {
+        checkBatchEdit();
+        final int remainingChars = mComposingText.length() - i;
+        if (remainingChars >= 0) {
+            mComposingText.setLength(remainingChars);
+        } else {
+            mComposingText.setLength(0);
+            // Never cut under 0
+            final int len = Math.max(mCommittedTextBeforeComposingText.length()
+                    + remainingChars, 0);
+            mCommittedTextBeforeComposingText.setLength(len);
+        }
+        if (mCurrentCursorPosition > i) {
+            mCurrentCursorPosition -= i;
+        } else {
+            mCurrentCursorPosition = 0;
+        }
+        if (null != mIC) {
+            mIC.deleteSurroundingText(i, j);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
+            }
+        }
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+    }
+
+    public void performEditorAction(final int actionId) {
+        mIC = mParent.getCurrentInputConnection();
+        if (null != mIC) {
+            mIC.performEditorAction(actionId);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_performEditorAction(actionId);
+            }
+        }
+    }
+
+    public void sendKeyEvent(final KeyEvent keyEvent) {
+        checkBatchEdit();
+        if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+            if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+            // This method is only called for enter or backspace when speaking to old
+            // applications (target SDK <= 15), or for digits.
+            // When talking to new applications we never use this method because it's inherently
+            // racy and has unpredictable results, but for backward compatibility we continue
+            // sending the key events for only Enter and Backspace because some applications
+            // mistakenly catch them to do some stuff.
+            switch (keyEvent.getKeyCode()) {
+                case KeyEvent.KEYCODE_ENTER:
+                    mCommittedTextBeforeComposingText.append("\n");
+                    mCurrentCursorPosition += 1;
+                    break;
+                case KeyEvent.KEYCODE_DEL:
+                    if (0 == mComposingText.length()) {
+                        if (mCommittedTextBeforeComposingText.length() > 0) {
+                            mCommittedTextBeforeComposingText.delete(
+                                    mCommittedTextBeforeComposingText.length() - 1,
+                                    mCommittedTextBeforeComposingText.length());
+                        }
+                    } else {
+                        mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
+                    }
+                    if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1;
+                    break;
+                case KeyEvent.KEYCODE_UNKNOWN:
+                    if (null != keyEvent.getCharacters()) {
+                        mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
+                        mCurrentCursorPosition += keyEvent.getCharacters().length();
+                    }
+                    break;
+                default:
+                    final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
+                    mCommittedTextBeforeComposingText.append(text);
+                    mCurrentCursorPosition += text.length();
+                    break;
+            }
+        }
+        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 (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+        mCurrentCursorPosition += text.length() - mComposingText.length();
+        mComposingText.setLength(0);
+        mComposingText.append(text);
+        // TODO: support values of i != 1. At this time, this is never called with i != 1.
+        if (null != mIC) {
+            mIC.setComposingText(text, i);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_setComposingText(text, i);
+            }
+        }
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+    }
+
+    public void setSelection(final int from, final int to) {
+        checkBatchEdit();
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+        if (null != mIC) {
+            mIC.setSelection(from, to);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_setSelection(from, to);
+            }
+        }
+        mCurrentCursorPosition = from;
+        mCommittedTextBeforeComposingText.setLength(0);
+        mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
+    }
+
+    public void commitCorrection(final CorrectionInfo correctionInfo) {
+        checkBatchEdit();
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+        // This has no effect on the text field and does not change its content. It only makes
+        // TextView flash the text for a second based on indices contained in the argument.
+        if (null != mIC) {
+            mIC.commitCorrection(correctionInfo);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_commitCorrection(correctionInfo);
+            }
+        }
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+    }
+
+    public void commitCompletion(final CompletionInfo completionInfo) {
+        checkBatchEdit();
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+        final CharSequence text = completionInfo.getText();
+        mCommittedTextBeforeComposingText.append(text);
+        mCurrentCursorPosition += text.length() - mComposingText.length();
+        mComposingText.setLength(0);
+        if (null != mIC) {
+            mIC.commitCompletion(completionInfo);
+            if (ProductionFlag.IS_EXPERIMENTAL) {
+                ResearchLogger.richInputConnection_commitCompletion(completionInfo);
+            }
+        }
+        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+    }
+
+    public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
+        mIC = mParent.getCurrentInputConnection();
+        if (null == mIC) return null;
+        final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+        if (DEBUG_PREVIOUS_TEXT && null != prev) {
+            final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
+            final String reference = prev.length() <= checkLength ? prev.toString()
+                    : prev.subSequence(prev.length() - checkLength, prev.length()).toString();
+            final StringBuilder internal = new StringBuilder()
+                    .append(mCommittedTextBeforeComposingText).append(mComposingText);
+            if (internal.length() > checkLength) {
+                internal.delete(0, internal.length() - checkLength);
+                if (!(reference.equals(internal.toString()))) {
+                    final String context =
+                            "Expected text = " + internal + "\nActual text = " + reference;
+                    ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
+                }
+            }
+        }
+        return getNthPreviousWord(prev, sentenceSeperators, n);
+    }
+
+    /**
+     * Represents a range of text, relative to the current cursor position.
+     */
+    public static class Range {
+        /** Characters before selection start */
+        public final int mCharsBefore;
+
+        /**
+         * Characters after selection start, including one trailing word
+         * separator.
+         */
+        public final int mCharsAfter;
+
+        /** The actual characters that make up a word */
+        public final String mWord;
+
+        public Range(int charsBefore, int charsAfter, String word) {
+            if (charsBefore < 0 || charsAfter < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+            this.mCharsBefore = charsBefore;
+            this.mCharsAfter = charsAfter;
+            this.mWord = word;
+        }
+    }
+
+    private static boolean isSeparator(int code, String sep) {
+        return sep.indexOf(code) != -1;
+    }
+
+    // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
+    // n = 2 retrieves the word before that, and so on. This splits on whitespace only.
+    // Also, it won't return words that end in a separator (if the nth word before the cursor
+    // ends in a separator, it returns null).
+    // Example :
+    // (n = 1) "abc def|" -> def
+    // (n = 1) "abc def |" -> def
+    // (n = 1) "abc def. |" -> null
+    // (n = 1) "abc def . |" -> null
+    // (n = 2) "abc def|" -> abc
+    // (n = 2) "abc def |" -> abc
+    // (n = 2) "abc def. |" -> abc
+    // (n = 2) "abc def . |" -> def
+    // (n = 2) "abc|" -> null
+    // (n = 2) "abc |" -> null
+    // (n = 2) "abc. def|" -> null
+    public static CharSequence getNthPreviousWord(final CharSequence prev,
+            final String sentenceSeperators, final int n) {
+        if (prev == null) return null;
+        String[] w = spaceRegex.split(prev);
+
+        // If we can't find n words, or we found an empty word, return null.
+        if (w.length < n || w[w.length - n].length() <= 0) return null;
+
+        // If ends in a separator, return null
+        char lastChar = w[w.length - n].charAt(w[w.length - n].length() - 1);
+        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+        return w[w.length - n];
+    }
+
+    /**
+     * @param separators characters which may separate words
+     * @return the word that surrounds the cursor, including up to one trailing
+     *   separator. For example, if the field contains "he|llo world", where |
+     *   represents the cursor, then "hello " will be returned.
+     */
+    public String getWordAtCursor(String separators) {
+        // getWordRangeAtCursor returns null if the connection is null
+        Range r = getWordRangeAtCursor(separators, 0);
+        return (r == null) ? null : r.mWord;
+    }
+
+    private int getCursorPosition() {
+        mIC = mParent.getCurrentInputConnection();
+        if (null == mIC) return INVALID_CURSOR_POSITION;
+        final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0);
+        if (extracted == null) {
+            return INVALID_CURSOR_POSITION;
+        }
+        return extracted.startOffset + extracted.selectionStart;
+    }
+
+    /**
+     * Returns the text surrounding the cursor.
+     *
+     * @param sep a string of characters that split words.
+     * @param additionalPrecedingWordsCount the number of words before the current word that should
+     *   be included in the returned range
+     * @return a range containing the text surrounding the cursor
+     */
+    public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) {
+        mIC = mParent.getCurrentInputConnection();
+        if (mIC == null || sep == null) {
+            return null;
+        }
+        CharSequence before = mIC.getTextBeforeCursor(1000, 0);
+        CharSequence after = mIC.getTextAfterCursor(1000, 0);
+        if (before == null || after == null) {
+            return null;
+        }
+
+        // Going backward, alternate skipping non-separators and separators until enough words
+        // have been read.
+        int start = before.length();
+        boolean isStoppingAtWhitespace = true;  // toggles to indicate what to stop at
+        while (true) { // see comments below for why this is guaranteed to halt
+            while (start > 0) {
+                final int codePoint = Character.codePointBefore(before, start);
+                if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+                    break;  // inner loop
+                }
+                --start;
+                if (Character.isSupplementaryCodePoint(codePoint)) {
+                    --start;
+                }
+            }
+            // isStoppingAtWhitespace is true every other time through the loop,
+            // so additionalPrecedingWordsCount is guaranteed to become < 0, which
+            // guarantees outer loop termination
+            if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) {
+                break;  // outer loop
+            }
+            isStoppingAtWhitespace = !isStoppingAtWhitespace;
+        }
+
+        // Find last word separator after the cursor
+        int end = -1;
+        while (++end < after.length()) {
+            final int codePoint = Character.codePointAt(after, end);
+            if (isSeparator(codePoint, sep)) {
+                break;
+            }
+            if (Character.isSupplementaryCodePoint(codePoint)) {
+                ++end;
+            }
+        }
+
+        int cursor = getCursorPosition();
+        if (start >= 0 && cursor + end <= after.length() + before.length()) {
+            String word = before.toString().substring(start, before.length())
+                    + after.toString().substring(0, end);
+            return new Range(before.length() - start, end, word);
+        }
+
+        return null;
+    }
+
+    public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
+        CharSequence before = getTextBeforeCursor(1, 0);
+        CharSequence after = getTextAfterCursor(1, 0);
+        if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
+                && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
+            return true;
+        }
+        if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
+                && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
+            return true;
+        }
+        return false;
+    }
+
+    public void removeTrailingSpace() {
+        checkBatchEdit();
+        final CharSequence lastOne = getTextBeforeCursor(1, 0);
+        if (lastOne != null && lastOne.length() == 1
+                && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
+            deleteSurroundingText(1, 0);
+        }
+    }
+
+    public boolean sameAsTextBeforeCursor(final CharSequence text) {
+        final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
+        return TextUtils.equals(text, beforeText);
+    }
+
+    /* (non-javadoc)
+     * Returns the word before the cursor if the cursor is at the end of a word, null otherwise
+     */
+    public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
+        // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
+        // separator or end of line/text)
+        // Example: "test|"<EOL> "te|st" get rejected here
+        final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
+        if (!TextUtils.isEmpty(textAfterCursor)
+                && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
+
+        // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
+        // Example: " -|" gets rejected here but "e-|" and "e|" are okay
+        CharSequence word = getWordAtCursor(settings.mWordSeparators);
+        // We don't suggest on leading single quotes, so we have to remove them from the word if
+        // it starts with single quotes.
+        while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
+            word = word.subSequence(1, word.length());
+        }
+        if (TextUtils.isEmpty(word)) return null;
+        // Find the last code point of the string
+        final int lastCodePoint = Character.codePointBefore(word, word.length());
+        // If for some reason the text field contains non-unicode binary data, or if the
+        // charsequence is exactly one char long and the contents is a low surrogate, return null.
+        if (!Character.isDefined(lastCodePoint)) return null;
+        // Bail out if the cursor is not at the end of a word (cursor must be preceded by
+        // non-whitespace, non-separator, non-start-of-text)
+        // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
+        if (settings.isWordSeparator(lastCodePoint)) return null;
+        final char firstChar = word.charAt(0); // we just tested that word is not empty
+        if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
+
+        // We only suggest on words that start with a letter or a symbol that is excluded from
+        // word separators (see #handleCharacterWhileInBatchEdit).
+        if (!(Character.isLetter(firstChar)
+                || settings.isSymbolExcludedFromWordSeparators(firstChar))) {
+            return null;
+        }
+
+        return word;
+    }
+
+    public boolean revertDoubleSpace() {
+        checkBatchEdit();
+        // Here we test whether we indeed have a period and a space before us. This should not
+        // be needed, but it's there just in case something went wrong.
+        final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+        if (!". ".equals(textBeforeCursor)) {
+            // Theoretically we should not be coming here if there isn't ". " before the
+            // cursor, but the application may be changing the text while we are typing, so
+            // anything goes. We should not crash.
+            Log.d(TAG, "Tried to revert double-space combo but we didn't find "
+                    + "\". \" just before the cursor.");
+            return false;
+        }
+        deleteSurroundingText(2, 0);
+        commitText("  ", 1);
+        return true;
+    }
+
+    public boolean revertSwapPunctuation() {
+        checkBatchEdit();
+        // Here we test whether we indeed have a space and something else before us. This should not
+        // be needed, but it's there just in case something went wrong.
+        final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+        // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
+        // enter surrogate pairs this code will have been removed.
+        if (TextUtils.isEmpty(textBeforeCursor)
+                || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
+            // We may only come here if the application is changing the text while we are typing.
+            // This is quite a broken case, but not logically impossible, so we shouldn't crash,
+            // but some debugging log may be in order.
+            Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
+                    + "find a space just before the cursor.");
+            return false;
+        }
+        deleteSurroundingText(2, 0);
+        commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
+        return true;
+    }
+
+    /**
+     * Heuristic to determine if this is an expected update of the cursor.
+     *
+     * Sometimes updates to the cursor position are late because of their asynchronous nature.
+     * This method tries to determine if this update is one, based on the values of the cursor
+     * position in the update, and the currently expected position of the cursor according to
+     * LatinIME's internal accounting. If this is not a belated expected update, then it should
+     * mean that the user moved the cursor explicitly.
+     * This is quite robust, but of course it's not perfect. In particular, it will fail in the
+     * case we get an update A, the user types in N characters so as to move the cursor to A+N but
+     * we don't get those, and then the user places the cursor between A and A+N, and we get only
+     * this update and not the ones in-between. This is almost impossible to achieve even trying
+     * very very hard.
+     *
+     * @param oldSelStart The value of the old cursor position in the update.
+     * @param newSelStart The value of the new cursor position in the update.
+     * @return whether this is a belated expected update or not.
+     */
+    public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) {
+        // If this is an update that arrives at our expected position, it's a belated update.
+        if (newSelStart == mCurrentCursorPosition) return true;
+        // If this is an update that moves the cursor from our expected position, it must be
+        // an explicit move.
+        if (oldSelStart == mCurrentCursorPosition) return false;
+        // The following returns true if newSelStart is between oldSelStart and
+        // mCurrentCursorPosition. We assume that if the updated position is between the old
+        // position and the expected position, then it must be a belated update.
+        return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 4bb2172..b984ec3 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -41,9 +41,8 @@
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethodcommon.InputMethodSettingsFragment;
 
-public class Settings extends InputMethodSettingsFragment
+public final class Settings extends InputMethodSettingsFragment
         implements SharedPreferences.OnSharedPreferenceChangeListener {
-    public static final boolean ENABLE_EXPERIMENTAL_SETTINGS = false;
 
     // In the same order as xml/prefs.xml
     public static final String PREF_GENERAL_SETTINGS = "general_settings";
@@ -57,25 +56,26 @@
     public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold";
     public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
     public static final String PREF_MISC_SETTINGS = "misc_settings";
-    public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
     public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME =
             "last_user_dictionary_write_time";
     public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings";
-    public static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
-            "pref_suppress_language_switch_key";
+    public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
+    public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY =
+            "pref_show_language_switch_key";
     public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
             "pref_include_other_imes_in_language_switch_list";
     public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles";
     public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
             "pref_key_preview_popup_dismiss_delay";
-    public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
-    public static final String PREF_BIGRAM_SUGGESTION = "next_word_suggestion";
     public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
-    public static final String PREF_KEY_ENABLE_SPAN_INSERT = "enable_span_insert";
+    public static final String PREF_GESTURE_INPUT = "gesture_input";
     public static final String PREF_VIBRATION_DURATION_SETTINGS =
             "pref_vibration_duration_settings";
     public static final String PREF_KEYPRESS_SOUND_VOLUME =
             "pref_keypress_sound_volume";
+    public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
+    public static final String PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT =
+            "pref_show_gesture_floating_preview_text";
 
     public static final String PREF_INPUT_LANGUAGE = "input_language";
     public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
@@ -87,27 +87,28 @@
     private ListPreference mShowCorrectionSuggestionsPreference;
     private ListPreference mAutoCorrectionThresholdPreference;
     private ListPreference mKeyPreviewPopupDismissDelay;
-    // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
-    private CheckBoxPreference mBigramSuggestion;
-    // Prediction: use bigrams to predict the next word when there is no input for it yet
+    // Use bigrams to predict the next word when there is no input for it yet
     private CheckBoxPreference mBigramPrediction;
     private Preference mDebugSettingsPreference;
 
     private TextView mKeypressVibrationDurationSettingsTextView;
     private TextView mKeypressSoundVolumeSettingsTextView;
 
+    private static void setPreferenceEnabled(final Preference preference, final boolean enabled) {
+        if (preference != null) {
+            preference.setEnabled(enabled);
+        }
+    }
+
     private void ensureConsistencyOfAutoCorrectionSettings() {
         final String autoCorrectionOff = getResources().getString(
                 R.string.auto_correction_threshold_mode_index_off);
         final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
-        mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff));
-        if (null != mBigramPrediction) {
-            mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
-        }
+        setPreferenceEnabled(mBigramPrediction, !currentSetting.equals(autoCorrectionOff));
     }
 
     @Override
-    public void onCreate(Bundle icicle) {
+    public void onCreate(final Bundle icicle) {
         super.onCreate(icicle);
         setInputMethodSettingsCategoryTitle(R.string.language_selection_title);
         setSubtypeEnablerTitle(R.string.select_language);
@@ -128,16 +129,7 @@
 
         mAutoCorrectionThresholdPreference =
                 (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
-        mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTION);
         mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS);
-        mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
-        if (mDebugSettingsPreference != null) {
-            final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
-            debugSettingsIntent.setClassName(
-                    context.getPackageName(), DebugSettings.class.getName());
-            mDebugSettingsPreference.setIntent(debugSettingsIntent);
-        }
-
         ensureConsistencyOfAutoCorrectionSettings();
 
         final PreferenceGroup generalSettings =
@@ -147,6 +139,18 @@
         final PreferenceGroup miscSettings =
                 (PreferenceGroup) findPreference(PREF_MISC_SETTINGS);
 
+        mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
+        if (mDebugSettingsPreference != null) {
+            if (ProductionFlag.IS_INTERNAL) {
+                final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
+                debugSettingsIntent.setClassName(
+                        context.getPackageName(), DebugSettingsActivity.class.getName());
+                mDebugSettingsPreference.setIntent(debugSettingsIntent);
+            } else {
+                miscSettings.removePreference(mDebugSettingsPreference);
+            }
+        }
+
         final boolean showVoiceKeyOption = res.getBoolean(
                 R.bool.config_enable_show_voice_key_option);
         if (!showVoiceKeyOption) {
@@ -155,9 +159,6 @@
 
         final PreferenceGroup advancedSettings =
                 (PreferenceGroup) findPreference(PREF_ADVANCED_SETTINGS);
-        // Remove those meaningless options for now. TODO: delete them for good
-        advancedSettings.removePreference(findPreference(PREF_BIGRAM_SUGGESTION));
-        advancedSettings.removePreference(findPreference(PREF_KEY_ENABLE_SPAN_INSERT));
         if (!VibratorUtils.getInstance(context).hasVibrator()) {
             generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
             if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
@@ -165,42 +166,34 @@
             }
         }
 
-        final boolean showPopupOption = res.getBoolean(
+        final boolean showKeyPreviewPopupOption = res.getBoolean(
                 R.bool.config_enable_show_popup_on_keypress_option);
-        if (!showPopupOption) {
-            generalSettings.removePreference(findPreference(PREF_POPUP_ON));
-        }
-
-        final boolean showBigramSuggestionsOption = res.getBoolean(
-                R.bool.config_enable_next_word_suggestions_option);
-        if (!showBigramSuggestionsOption) {
-            textCorrectionGroup.removePreference(mBigramSuggestion);
-            if (null != mBigramPrediction) {
-                textCorrectionGroup.removePreference(mBigramPrediction);
-            }
-        }
-
-        final CheckBoxPreference includeOtherImesInLanguageSwitchList =
-                (CheckBoxPreference)findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
-        includeOtherImesInLanguageSwitchList.setEnabled(
-                !SettingsValues.isLanguageSwitchKeySupressed(prefs));
-
         mKeyPreviewPopupDismissDelay =
-                (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
-        final String[] entries = new String[] {
-                res.getString(R.string.key_preview_popup_dismiss_no_delay),
-                res.getString(R.string.key_preview_popup_dismiss_default_delay),
-        };
-        final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
-                R.integer.config_key_preview_linger_timeout));
-        mKeyPreviewPopupDismissDelay.setEntries(entries);
-        mKeyPreviewPopupDismissDelay.setEntryValues(
-                new String[] { "0", popupDismissDelayDefaultValue });
-        if (null == mKeyPreviewPopupDismissDelay.getValue()) {
-            mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
+                (ListPreference) findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+        if (!showKeyPreviewPopupOption) {
+            generalSettings.removePreference(findPreference(PREF_POPUP_ON));
+            if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
+                advancedSettings.removePreference(mKeyPreviewPopupDismissDelay);
+            }
+        } else {
+            final String[] entries = new String[] {
+                    res.getString(R.string.key_preview_popup_dismiss_no_delay),
+                    res.getString(R.string.key_preview_popup_dismiss_default_delay),
+            };
+            final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
+                    R.integer.config_key_preview_linger_timeout));
+            mKeyPreviewPopupDismissDelay.setEntries(entries);
+            mKeyPreviewPopupDismissDelay.setEntryValues(
+                    new String[] { "0", popupDismissDelayDefaultValue });
+            if (null == mKeyPreviewPopupDismissDelay.getValue()) {
+                mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
+            }
+            setPreferenceEnabled(mKeyPreviewPopupDismissDelay,
+                    SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
         }
-        mKeyPreviewPopupDismissDelay.setEnabled(
-                SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
+
+        setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
+                SettingsValues.showsLanguageSwitchKey(prefs));
 
         final PreferenceScreen dictionaryLink =
                 (PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY);
@@ -211,21 +204,19 @@
             textCorrectionGroup.removePreference(dictionaryLink);
         }
 
-        final boolean showUsabilityStudyModeOption =
-                res.getBoolean(R.bool.config_enable_usability_study_mode_option)
-                        || ProductionFlag.IS_EXPERIMENTAL || ENABLE_EXPERIMENTAL_SETTINGS;
-        final Preference usabilityStudyPref = findPreference(PREF_USABILITY_STUDY_MODE);
-        if (!showUsabilityStudyModeOption) {
-            if (usabilityStudyPref != null) {
-                miscSettings.removePreference(usabilityStudyPref);
-            }
-        }
-        if (ProductionFlag.IS_EXPERIMENTAL) {
-            if (usabilityStudyPref instanceof CheckBoxPreference) {
-                CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
-                checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE, true));
-                checkbox.setSummary(R.string.settings_warning_researcher_mode);
-            }
+        final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
+                R.bool.config_gesture_input_enabled_by_build_config);
+        final Preference gesturePreviewTrail = findPreference(PREF_GESTURE_PREVIEW_TRAIL);
+        final Preference gestureFloatingPreviewText = findPreference(
+                PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT);
+        if (!gestureInputEnabledByBuildConfig) {
+            miscSettings.removePreference(findPreference(PREF_GESTURE_INPUT));
+            miscSettings.removePreference(gesturePreviewTrail);
+            miscSettings.removePreference(gestureFloatingPreviewText);
+        } else {
+            final boolean gestureInputEnabledByUser = prefs.getBoolean(PREF_GESTURE_INPUT, true);
+            setPreferenceEnabled(gesturePreviewTrail, gestureInputEnabledByUser);
+            setPreferenceEnabled(gestureFloatingPreviewText, gestureInputEnabledByUser);
         }
 
         mKeypressVibrationDurationSettingsPref =
@@ -280,20 +271,25 @@
     }
 
     @Override
-    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
         (new BackupManager(getActivity())).dataChanged();
         if (key.equals(PREF_POPUP_ON)) {
-            final ListPreference popupDismissDelay =
-                (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
-            if (null != popupDismissDelay) {
-                popupDismissDelay.setEnabled(prefs.getBoolean(PREF_POPUP_ON, true));
+            setPreferenceEnabled(findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY),
+                    prefs.getBoolean(PREF_POPUP_ON, true));
+        } else if (key.equals(PREF_SHOW_LANGUAGE_SWITCH_KEY)) {
+            setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
+                    SettingsValues.showsLanguageSwitchKey(prefs));
+        } else if (key.equals(PREF_GESTURE_INPUT)) {
+            final boolean gestureInputEnabledByConfig = getResources().getBoolean(
+                    R.bool.config_gesture_input_enabled_by_build_config);
+            if (gestureInputEnabledByConfig) {
+                final boolean gestureInputEnabledByUser = prefs.getBoolean(
+                        PREF_GESTURE_INPUT, true);
+                setPreferenceEnabled(findPreference(PREF_GESTURE_PREVIEW_TRAIL),
+                        gestureInputEnabledByUser);
+                setPreferenceEnabled(findPreference(PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT),
+                        gestureInputEnabledByUser);
             }
-        } else if (key.equals(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
-            final CheckBoxPreference includeOtherImesInLanguageSwicthList =
-                    (CheckBoxPreference)findPreference(
-                            PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
-            includeOtherImesInLanguageSwicthList.setEnabled(
-                    !SettingsValues.isLanguageSwitchKeySupressed(prefs));
         }
         ensureConsistencyOfAutoCorrectionSettings();
         updateVoiceModeSummary();
@@ -327,33 +323,37 @@
 
     private void updateKeyPreviewPopupDelaySummary() {
         final ListPreference lp = mKeyPreviewPopupDismissDelay;
-        lp.setSummary(lp.getEntries()[lp.findIndexOfValue(lp.getValue())]);
+        final CharSequence[] entries = lp.getEntries();
+        if (entries == null || entries.length <= 0) return;
+        lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]);
     }
 
     private void updateVoiceModeSummary() {
         mVoicePreference.setSummary(
                 getResources().getStringArray(R.array.voice_input_modes_summary)
-                [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
+                        [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
     }
 
     private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
-            SharedPreferences sp, Resources res) {
+            final SharedPreferences sp, final Resources res) {
         if (mKeypressVibrationDurationSettingsPref != null) {
-            final boolean hasVibrator = VibratorUtils.getInstance(getActivity()).hasVibrator();
-            final boolean vibrateOn = hasVibrator && sp.getBoolean(Settings.PREF_VIBRATE_ON,
+            final boolean hasVibratorHardware = VibratorUtils.getInstance(getActivity())
+                    .hasVibrator();
+            final boolean vibrateOnByUser = sp.getBoolean(Settings.PREF_VIBRATE_ON,
                     res.getBoolean(R.bool.config_default_vibration_enabled));
-            mKeypressVibrationDurationSettingsPref.setEnabled(vibrateOn);
+            setPreferenceEnabled(mKeypressVibrationDurationSettingsPref,
+                    hasVibratorHardware && vibrateOnByUser);
         }
 
         if (mKeypressSoundVolumeSettingsPref != null) {
             final boolean soundOn = sp.getBoolean(Settings.PREF_SOUND_ON,
                     res.getBoolean(R.bool.config_default_sound_enabled));
-            mKeypressSoundVolumeSettingsPref.setEnabled(soundOn);
+            setPreferenceEnabled(mKeypressSoundVolumeSettingsPref, soundOn);
         }
     }
 
     private void updateKeypressVibrationDurationSettingsSummary(
-            SharedPreferences sp, Resources res) {
+            final SharedPreferences sp, final Resources res) {
         if (mKeypressVibrationDurationSettingsPref != null) {
             mKeypressVibrationDurationSettingsPref.setSummary(
                     SettingsValues.getCurrentVibrationDuration(sp, res)
@@ -411,7 +411,7 @@
         builder.create().show();
     }
 
-    private void updateKeypressSoundVolumeSummary(SharedPreferences sp, Resources res) {
+    private void updateKeypressSoundVolumeSummary(final SharedPreferences sp, final Resources res) {
         if (mKeypressSoundVolumeSettingsPref != null) {
             mKeypressSoundVolumeSettingsPref.setSummary(String.valueOf(
                     (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * 100)));
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index b07c3e5..5b8f1cf 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.util.Log;
 import android.view.inputmethod.EditorInfo;
@@ -29,15 +30,27 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Map;
 
 /**
  * When you call the constructor of this class, you may want to change the current system locale by
  * using {@link LocaleUtils.RunInLocale}.
  */
-public class SettingsValues {
+public final class SettingsValues {
     private static final String TAG = SettingsValues.class.getSimpleName();
 
+    private static final int SUGGESTION_VISIBILITY_SHOW_VALUE
+            = R.string.prefs_suggestion_visibility_show_value;
+    private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+            = R.string.prefs_suggestion_visibility_show_only_portrait_value;
+    private static final int SUGGESTION_VISIBILITY_HIDE_VALUE
+            = R.string.prefs_suggestion_visibility_hide_value;
+
+    private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
+        SUGGESTION_VISIBILITY_SHOW_VALUE,
+        SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE,
+        SUGGESTION_VISIBILITY_HIDE_VALUE
+    };
+
     // From resources:
     public final int mDelayUpdateOldSuggestions;
     public final String mWeakSpaceStrippers;
@@ -59,31 +72,37 @@
     @SuppressWarnings("unused") // TODO: Use this
     private final boolean mUsabilityStudyMode;
     public final boolean mIncludesOtherImesInLanguageSwitchList;
-    public final boolean mIsLanguageSwitchKeySuppressed;
+    public final boolean mShowsLanguageSwitchKey;
     @SuppressWarnings("unused") // TODO: Use this
     private final String mKeyPreviewPopupDismissDelayRawValue;
     public final boolean mUseContactsDict;
-    // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
-    public final boolean mBigramSuggestionEnabled;
-    // Prediction: use bigrams to predict the next word when there is no input for it yet
+    // Use bigrams to predict the next word when there is no input for it yet
     public final boolean mBigramPredictionEnabled;
-    public final boolean mEnableSuggestionSpanInsertion;
     @SuppressWarnings("unused") // TODO: Use this
     private final int mVibrationDurationSettingsRawValue;
     @SuppressWarnings("unused") // TODO: Use this
     private final float mKeypressSoundVolumeRawValue;
     private final InputMethodSubtype[] mAdditionalSubtypes;
+    public final boolean mGestureInputEnabled;
+    public final boolean mGesturePreviewTrailEnabled;
+    public final boolean mGestureFloatingPreviewTextEnabled;
+
+    // From the input box
+    private final InputAttributes mInputAttributes;
 
     // Deduced settings
     public final int mKeypressVibrationDuration;
     public final float mFxVolume;
     public final int mKeyPreviewPopupDismissDelay;
-    public final boolean mAutoCorrectEnabled;
+    private final boolean mAutoCorrectEnabled;
     public final float mAutoCorrectionThreshold;
+    public final boolean mCorrectionEnabled;
+    public final int mSuggestionVisibility;
     private final boolean mVoiceKeyEnabled;
     private final boolean mVoiceKeyOnMain;
 
-    public SettingsValues(final SharedPreferences prefs, final Context context) {
+    public SettingsValues(final SharedPreferences prefs, final InputAttributes inputAttributes,
+            final Context context) {
         final Resources res = context.getResources();
 
         // Get the resources
@@ -109,6 +128,13 @@
                 mSymbolsExcludedFromWordSeparators, res);
         mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
 
+        // Store the input attributes
+        if (null == inputAttributes) {
+            mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+        } else {
+            mInputAttributes = inputAttributes;
+        }
+
         // Get the settings preferences
         mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
         mVibrateOn = isVibrateOn(context, prefs, res);
@@ -125,18 +151,13 @@
         mUsabilityStudyMode = getUsabilityStudyMode(prefs);
         mIncludesOtherImesInLanguageSwitchList = prefs.getBoolean(
                 Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false);
-        mIsLanguageSwitchKeySuppressed = isLanguageSwitchKeySupressed(prefs);
+        mShowsLanguageSwitchKey = showsLanguageSwitchKey(prefs);
         mKeyPreviewPopupDismissDelayRawValue = prefs.getString(
                 Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
                 Integer.toString(res.getInteger(R.integer.config_key_preview_linger_timeout)));
         mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
         mAutoCorrectEnabled = isAutoCorrectEnabled(res, mAutoCorrectionThresholdRawValue);
-        mBigramSuggestionEnabled = mAutoCorrectEnabled
-                && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled);
-        mBigramPredictionEnabled = mBigramSuggestionEnabled
-                && isBigramPredictionEnabled(prefs, res);
-        // TODO: remove mEnableSuggestionSpanInsertion. It's always true.
-        mEnableSuggestionSpanInsertion = true;
+        mBigramPredictionEnabled = isBigramPredictionEnabled(prefs, res);
         mVibrationDurationSettingsRawValue =
                 prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
         mKeypressSoundVolumeRawValue = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
@@ -151,22 +172,30 @@
         mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain);
         mAdditionalSubtypes = AdditionalSubtype.createAdditionalSubtypesArray(
                 getPrefAdditionalSubtypes(prefs, res));
+        final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
+                R.bool.config_gesture_input_enabled_by_build_config);
+        mGestureInputEnabled = gestureInputEnabledByBuildConfig
+                && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true);
+        mGesturePreviewTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
+        mGestureFloatingPreviewTextEnabled = prefs.getBoolean(
+                Settings.PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT, false);
+        mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
+        mSuggestionVisibility = createSuggestionVisibility(res);
     }
 
     // Helper functions to create member values.
     private static SuggestedWords createSuggestPuncList(final String[] puncs) {
-        final ArrayList<SuggestedWords.SuggestedWordInfo> puncList =
-                new ArrayList<SuggestedWords.SuggestedWordInfo>();
+        final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList();
         if (puncs != null) {
             for (final String puncSpec : puncs) {
-                puncList.add(new SuggestedWords.SuggestedWordInfo(
-                        KeySpecParser.getLabel(puncSpec), SuggestedWordInfo.MAX_SCORE));
+                puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
+                        SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED,
+                        Dictionary.TYPE_HARDCODED));
             }
         }
         return new SuggestedWords(puncList,
                 false /* typedWordValid */,
                 false /* hasAutoCorrectionCandidate */,
-                false /* allowsToBeAutoCorrected */,
                 true /* isPunctuationSuggestions */,
                 false /* isObsoleteSuggestions */,
                 false /* isPrediction */);
@@ -184,6 +213,16 @@
         return wordSeparators;
     }
 
+    private int createSuggestionVisibility(final Resources res) {
+        final String suggestionVisiblityStr = mShowSuggestionsSetting;
+        for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
+            if (suggestionVisiblityStr.equals(res.getString(visibility))) {
+                return visibility;
+            }
+        }
+        throw new RuntimeException("Bug: visibility string is not configured correctly");
+    }
+
     private static boolean isVibrateOn(final Context context, final SharedPreferences prefs,
             final Resources res) {
         final boolean hasVibrator = VibratorUtils.getInstance(context).hasVibrator();
@@ -191,70 +230,81 @@
                 res.getBoolean(R.bool.config_default_vibration_enabled));
     }
 
-    public boolean isWordSeparator(int code) {
+    public boolean isApplicationSpecifiedCompletionsOn() {
+        return mInputAttributes.mApplicationSpecifiedCompletionOn;
+    }
+
+    public boolean isSuggestionsRequested(final int displayOrientation) {
+        return mInputAttributes.mIsSettingsSuggestionStripOn
+                && (mCorrectionEnabled
+                        || isSuggestionStripVisibleInOrientation(displayOrientation));
+    }
+
+    public boolean isSuggestionStripVisibleInOrientation(final int orientation) {
+        return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE)
+                || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+                        && orientation == Configuration.ORIENTATION_PORTRAIT);
+    }
+
+    public boolean isWordSeparator(final int code) {
         return mWordSeparators.contains(String.valueOf((char)code));
     }
 
-    public boolean isSymbolExcludedFromWordSeparators(int code) {
+    public boolean isSymbolExcludedFromWordSeparators(final int code) {
         return mSymbolsExcludedFromWordSeparators.contains(String.valueOf((char)code));
     }
 
-    public boolean isWeakSpaceStripper(int code) {
+    public boolean isWeakSpaceStripper(final int code) {
         // TODO: this does not work if the code does not fit in a char
         return mWeakSpaceStrippers.contains(String.valueOf((char)code));
     }
 
-    public boolean isWeakSpaceSwapper(int code) {
+    public boolean isWeakSpaceSwapper(final int code) {
         // TODO: this does not work if the code does not fit in a char
         return mWeakSpaceSwappers.contains(String.valueOf((char)code));
     }
 
-    public boolean isPhantomSpacePromotingSymbol(int code) {
+    public boolean isPhantomSpacePromotingSymbol(final int code) {
         // TODO: this does not work if the code does not fit in a char
         return mPhantomSpacePromotingSymbols.contains(String.valueOf((char)code));
     }
 
-    private static boolean isAutoCorrectEnabled(final Resources resources,
+    private static boolean isAutoCorrectEnabled(final Resources res,
             final String currentAutoCorrectionSetting) {
-        final String autoCorrectionOff = resources.getString(
+        final String autoCorrectionOff = res.getString(
                 R.string.auto_correction_threshold_mode_index_off);
         return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
     }
 
     // Public to access from KeyboardSwitcher. Should it have access to some
     // process-global instance instead?
-    public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) {
-        final boolean showPopupOption = resources.getBoolean(
+    public static boolean isKeyPreviewPopupEnabled(final SharedPreferences prefs,
+            final Resources res) {
+        final boolean showPopupOption = res.getBoolean(
                 R.bool.config_enable_show_popup_on_keypress_option);
-        if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview);
-        return sp.getBoolean(Settings.PREF_POPUP_ON,
-                resources.getBoolean(R.bool.config_default_popup_preview));
+        if (!showPopupOption) return res.getBoolean(R.bool.config_default_popup_preview);
+        return prefs.getBoolean(Settings.PREF_POPUP_ON,
+                res.getBoolean(R.bool.config_default_popup_preview));
     }
 
     // Likewise
-    public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp,
-            Resources resources) {
+    public static int getKeyPreviewPopupDismissDelay(final SharedPreferences prefs,
+            final Resources res) {
         // TODO: use mKeyPreviewPopupDismissDelayRawValue instead of reading it again here.
-        return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
-                Integer.toString(resources.getInteger(
+        return Integer.parseInt(prefs.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+                Integer.toString(res.getInteger(
                         R.integer.config_key_preview_linger_timeout))));
     }
 
-    private static boolean isBigramSuggestionEnabled(final SharedPreferences sp,
-            final Resources resources, final boolean autoCorrectEnabled) {
-        // TODO: remove this method. Bigram suggestion is always true.
-        return true;
-    }
-
-    private static boolean isBigramPredictionEnabled(final SharedPreferences sp,
-            final Resources resources) {
-        return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
+    private static boolean isBigramPredictionEnabled(final SharedPreferences prefs,
+            final Resources res) {
+        return prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, res.getBoolean(
                 R.bool.config_default_next_word_prediction));
     }
 
-    private static float getAutoCorrectionThreshold(final Resources resources,
+    private static float getAutoCorrectionThreshold(final Resources res,
             final String currentAutoCorrectionSetting) {
-        final String[] autoCorrectionThresholdValues = resources.getStringArray(
+        final String[] autoCorrectionThresholdValues = res.getStringArray(
                 R.array.auto_correction_threshold_values);
         // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off.
         float autoCorrectionThreshold = Float.MAX_VALUE;
@@ -286,12 +336,25 @@
         return mVoiceKeyOnMain;
     }
 
-    public static boolean isLanguageSwitchKeySupressed(SharedPreferences sp) {
-        return sp.getBoolean(Settings.PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false);
+    // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
+    // This is being used only for the backward compatibility.
+    private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
+            "pref_suppress_language_switch_key";
+
+    public static boolean showsLanguageSwitchKey(final SharedPreferences prefs) {
+        if (prefs.contains(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
+            final boolean suppressLanguageSwitchKey = prefs.getBoolean(
+                    PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false);
+            final SharedPreferences.Editor editor = prefs.edit();
+            editor.remove(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY);
+            editor.putBoolean(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, !suppressLanguageSwitchKey);
+            editor.apply();
+        }
+        return prefs.getBoolean(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, true);
     }
 
-    public boolean isLanguageSwitchKeyEnabled(Context context) {
-        if (mIsLanguageSwitchKeySuppressed) {
+    public boolean isLanguageSwitchKeyEnabled(final Context context) {
+        if (!mShowsLanguageSwitchKey) {
             return false;
         }
         if (mIncludesOtherImesInLanguageSwitchList) {
@@ -303,7 +366,7 @@
         }
     }
 
-    public boolean isFullscreenModeAllowed(Resources res) {
+    public static boolean isFullscreenModeAllowed(final Resources res) {
         return res.getBoolean(R.bool.config_use_fullscreen_mode);
     }
 
@@ -313,58 +376,68 @@
 
     public static String getPrefAdditionalSubtypes(final SharedPreferences prefs,
             final Resources res) {
-        final String prefSubtypes = res.getString(R.string.predefined_subtypes, "");
-        return prefs.getString(Settings.PREF_CUSTOM_INPUT_STYLES, prefSubtypes);
+        final String predefinedPrefSubtypes = AdditionalSubtype.createPrefSubtypes(
+                res.getStringArray(R.array.predefined_subtypes));
+        return prefs.getString(Settings.PREF_CUSTOM_INPUT_STYLES, predefinedPrefSubtypes);
     }
 
     // Accessed from the settings interface, hence public
-    public static float getCurrentKeypressSoundVolume(final SharedPreferences sp,
-                final Resources res) {
+    public static float getCurrentKeypressSoundVolume(final SharedPreferences prefs,
+            final Resources res) {
         // TODO: use mVibrationDurationSettingsRawValue instead of reading it again here
-        final float volume = sp.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
+        final float volume = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
         if (volume >= 0) {
             return volume;
         }
 
-        return Float.parseFloat(
-                Utils.getDeviceOverrideValue(res, R.array.keypress_volumes, "-1.0f"));
+        return Float.parseFloat(ResourceUtils.getDeviceOverrideValue(
+                res, R.array.keypress_volumes, "-1.0f"));
     }
 
     // Likewise
-    public static int getCurrentVibrationDuration(final SharedPreferences sp,
-                final Resources res) {
+    public static int getCurrentVibrationDuration(final SharedPreferences prefs,
+            final Resources res) {
         // TODO: use mKeypressVibrationDuration instead of reading it again here
-        final int ms = sp.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
+        final int ms = prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
         if (ms >= 0) {
             return ms;
         }
 
-        return Integer.parseInt(
-                Utils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations, "-1"));
+        return Integer.parseInt(ResourceUtils.getDeviceOverrideValue(
+                res, R.array.keypress_vibration_durations, "-1"));
     }
 
     // Likewise
     public static boolean getUsabilityStudyMode(final SharedPreferences prefs) {
         // TODO: use mUsabilityStudyMode instead of reading it again here
-        return prefs.getBoolean(Settings.PREF_USABILITY_STUDY_MODE, true);
+        return prefs.getBoolean(DebugSettings.PREF_USABILITY_STUDY_MODE, true);
     }
 
-    public static long getLastUserHistoryWriteTime(
-            final SharedPreferences prefs, final String locale) {
+    public static long getLastUserHistoryWriteTime(final SharedPreferences prefs,
+            final String locale) {
         final String str = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
-        final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(str);
+        final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(str);
         if (map.containsKey(locale)) {
             return map.get(locale);
         }
         return 0;
     }
 
-    public static void setLastUserHistoryWriteTime(
-            final SharedPreferences prefs, final String locale) {
+    public static void setLastUserHistoryWriteTime(final SharedPreferences prefs,
+            final String locale) {
         final String oldStr = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
-        final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(oldStr);
+        final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(oldStr);
         map.put(locale, System.currentTimeMillis());
-        final String newStr = Utils.localeAndTimeHashMapToStr(map);
+        final String newStr = LocaleUtils.localeAndTimeHashMapToStr(map);
         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 a43b905..7b65b73 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -18,10 +18,12 @@
 
 import android.text.TextUtils;
 
+import com.android.inputmethod.keyboard.Keyboard; // For character constants
+
 import java.util.ArrayList;
 import java.util.Locale;
 
-public class StringUtils {
+public final class StringUtils {
     private StringUtils() {
         // This utility class is not publicly instantiable.
     }
@@ -53,7 +55,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);
         }
@@ -123,23 +125,6 @@
     }
 
     /**
-     * Returns true if cs contains any upper case characters.
-     *
-     * @param cs the CharSequence to check
-     * @return {@code true} if cs contains any upper case characters, {@code false} otherwise.
-     */
-    public static boolean hasUpperCase(final CharSequence cs) {
-        final int length = cs.length();
-        for (int i = 0, cp = 0; i < length; i += Character.charCount(cp)) {
-            cp = Character.codePointAt(cs, i);
-            if (Character.isUpperCase(cp)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
      * Remove duplicates from an array of strings.
      *
      * This method will always keep the first occurrence of all strings at their position
@@ -184,6 +169,9 @@
         final char[] characters = string.toCharArray();
         final int length = characters.length;
         final int[] codePoints = new int[Character.codePointCount(characters, 0, length)];
+        if (length <= 0) {
+            return new int[0];
+        }
         int codePoint = Character.codePointAt(characters, 0);
         int dsti = 0;
         for (int srci = Character.charCount(codePoint);
@@ -194,4 +182,207 @@
         codePoints[dsti] = codePoint;
         return codePoints;
     }
+
+    /**
+     * Determine what caps mode should be in effect at the current offset in
+     * the text. Only the mode bits set in <var>reqModes</var> will be
+     * checked. Note that the caps mode flags here are explicitly defined
+     * to match those in {@link InputType}.
+     *
+     * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting
+     * issues). This will change in the future as we simplify the code for our use and fix bugs.
+     *
+     * @param cs The text that should be checked for caps modes.
+     * @param reqModes The modes to be checked: may be any combination of
+     * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+     * {@link TextUtils#CAP_MODE_SENTENCES}.
+     * @param locale The locale to consider for capitalization rules
+     * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs
+     *
+     * @return Returns the actual capitalization modes that can be in effect
+     * at the current position, which is any combination of
+     * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+     * {@link TextUtils#CAP_MODE_SENTENCES}.
+     */
+    public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale,
+            final boolean hasSpaceBefore) {
+        // Quick description of what we want to do:
+        // CAP_MODE_CHARACTERS is always on.
+        // CAP_MODE_WORDS is on if there is some whitespace before the cursor.
+        // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end
+        //   of a sentence just before that.
+        // We ignore opening parentheses and the like just before the cursor for purposes of
+        // finding whitespace for WORDS and SENTENCES modes.
+        // The end of a sentence ends with a period, question mark or exclamation mark. If it's
+        // a period, it also needs not to be an abbreviation, which means it also needs to either
+        // be immediately preceded by punctuation, or by a string of only letters with single
+        // periods interleaved.
+
+        // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on.
+        if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) {
+            // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already
+            // evaluated MODE_CHARACTERS, we can return.
+            return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+        }
+
+        // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes
+        // opening parentheses, brackets, opening quotes, everything that *opens* a span of
+        // text in the linguistic sense. In RTL languages, this is still an opening sign, although
+        // it may look like a right parenthesis for example. We also include double quote and
+        // single quote since they aren't start punctuation in the unicode sense, but should still
+        // be skipped for English. TODO: does this depend on the language?
+        int i;
+        if (hasSpaceBefore) {
+            i = cs.length() + 1;
+        } else {
+            for (i = cs.length(); i > 0; i--) {
+                final char c = cs.charAt(i - 1);
+                if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE
+                        && Character.getType(c) != Character.START_PUNCTUATION) {
+                    break;
+                }
+            }
+        }
+
+        // We are now on the character that precedes any starting punctuation, so in the most
+        // frequent case this will be whitespace or a letter, although it may occasionally be a
+        // start of line, or some symbol.
+
+        // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2,
+        // we go back over any space or tab char sitting there. We find the start of a paragraph
+        // if the first char that's not a space or tab is a start of line (as in, either \n or
+        // start of text).
+        int j = i;
+        if (hasSpaceBefore) --j;
+        while (j > 0 && Character.isWhitespace(cs.charAt(j - 1))) {
+            j--;
+        }
+        if (j == 0) {
+            // There is only whitespace between the start of the text and the cursor. Both
+            // MODE_WORDS and MODE_SENTENCES should be active.
+            return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+                    | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+        }
+        if (i == j) {
+            // If we don't have whitespace before index i, it means neither MODE_WORDS
+            // nor mode sentences should be on so we can return right away.
+            return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+        }
+        if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) {
+            // Here we know we have whitespace before the cursor (if not, we returned in the above
+            // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate
+            // MODE_SENTENCES so we can return right away.
+            return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+        }
+        // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above,
+        // we know that MODE_SENTENCES is being requested.
+
+        // Step 4 : Search for MODE_SENTENCES.
+        // English is a special case in that "American typography" rules, which are the most common
+        // in English, state that a sentence terminator immediately following a quotation mark
+        // should be swapped with it and de-duplicated (included in the quotation mark),
+        // e.g. <<Did he say, "let's go home?">>
+        // No other language has such a rule as far as I know, instead putting inside the quotation
+        // mark as the exact thing quoted and handling the surrounding punctuation independently,
+        // e.g. <<Did he say, "let's go home"?>>
+        // Hence, specifically for English, we treat this special case here.
+        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
+            for (; j > 0; j--) {
+                // Here we look to go over any closing punctuation. This is because in dominant
+                // variants of English, the final period is placed within double quotes and maybe
+                // other closing punctuation signs. This is generally not true in other languages.
+                final char c = cs.charAt(j - 1);
+                if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE
+                        && Character.getType(c) != Character.END_PUNCTUATION) {
+                    break;
+                }
+            }
+        }
+
+        if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+        char c = cs.charAt(--j);
+
+        // We found the next interesting chunk of text ; next we need to determine if it's the
+        // end of a sentence. If we have a question mark or an exclamation mark, it's the end of
+        // a sentence. If it's neither, the only remaining case is the period so we get the opposite
+        // case out of the way.
+        if (c == Keyboard.CODE_QUESTION_MARK || c == Keyboard.CODE_EXCLAMATION_MARK) {
+            return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+        }
+        if (c != Keyboard.CODE_PERIOD || j <= 0) {
+            return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+        }
+
+        // We found out that we have a period. We need to determine if this is a full stop or
+        // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation
+        // looks like (\w\.){2,}
+        // To find out, we will have a simple state machine with the following states :
+        // START, WORD, PERIOD, ABBREVIATION
+        // On START : (just before the first period)
+        //           letter => WORD
+        //           whitespace => end with no caps (it was a stand-alone period)
+        //           otherwise => end with caps (several periods/symbols in a row)
+        // On WORD : (within the word just before the first period)
+        //           letter => WORD
+        //           period => PERIOD
+        //           otherwise => end with caps (it was a word with a full stop at the end)
+        // On PERIOD : (period within a potential abbreviation)
+        //           letter => LETTER
+        //           otherwise => end with caps (it was not an abbreviation)
+        // On LETTER : (letter within a potential abbreviation)
+        //           letter => LETTER
+        //           period => PERIOD
+        //           otherwise => end with no caps (it was an abbreviation)
+        // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This
+        // should capitalize.
+
+        final int START = 0;
+        final int WORD = 1;
+        final int PERIOD = 2;
+        final int LETTER = 3;
+        final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+                | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+        final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+        int state = START;
+        while (j > 0) {
+            c = cs.charAt(--j);
+            switch (state) {
+            case START:
+                if (Character.isLetter(c)) {
+                    state = WORD;
+                } else if (Character.isWhitespace(c)) {
+                    return noCaps;
+                } else {
+                    return caps;
+                }
+                break;
+            case WORD:
+                if (Character.isLetter(c)) {
+                    state = WORD;
+                } else if (c == Keyboard.CODE_PERIOD) {
+                    state = PERIOD;
+                } else {
+                    return caps;
+                }
+                break;
+            case PERIOD:
+                if (Character.isLetter(c)) {
+                    state = LETTER;
+                } else {
+                    return caps;
+                }
+                break;
+            case LETTER:
+                if (Character.isLetter(c)) {
+                    state = LETTER;
+                } else if (c == Keyboard.CODE_PERIOD) {
+                    state = PERIOD;
+                } else {
+                    return noCaps;
+                }
+            }
+        }
+        // Here we arrived at the start of the line. This should behave exactly like whitespace.
+        return (START == state || LETTER == state) ? noCaps : caps;
+    }
 }
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 664de67..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,26 +81,25 @@
         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(
                 Context.CONNECTIVITY_SERVICE);
         mCurrentSystemLocale = mResources.getConfiguration().locale;
-        mCurrentSubtype = mImm.getCurrentInputMethodSubtype();
         mNoLanguageSubtype = ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet(
                 service, SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
+        mCurrentSubtype = ImfUtils.getCurrentInputMethodSubtype(service, mNoLanguageSubtype);
         if (mNoLanguageSubtype == null) {
             throw new RuntimeException("Can't find no lanugage with QWERTY subtype");
         }
@@ -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(mImm.getCurrentInputMethodSubtype());
-        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(mImm.getCurrentInputMethodSubtype());
         }
+        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 336a76f..278c4b9 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.ProximityInfo;
@@ -26,6 +25,7 @@
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.concurrent.ConcurrentHashMap;
@@ -34,87 +34,55 @@
  * This class loads a dictionary and provides a list of suggestions for a given sequence of
  * characters. This includes corrections and completions.
  */
-public class Suggest implements Dictionary.WordCallback {
+public class Suggest {
     public static final String TAG = Suggest.class.getSimpleName();
 
-    public static final int APPROX_MAX_WORD_LENGTH = 32;
+    // Session id for
+    // {@link #getSuggestedWords(WordComposer,CharSequence,ProximityInfo,boolean,int)}.
+    public static final int SESSION_TYPING = 0;
+    public static final int SESSION_GESTURE = 1;
 
+    // TODO: rename this to CORRECTION_OFF
     public static final int CORRECTION_NONE = 0;
+    // TODO: rename this to CORRECTION_ON
     public static final int CORRECTION_FULL = 1;
-    public static final int CORRECTION_FULL_BIGRAM = 2;
 
-    // It seems the following values are only used for logging.
-    public static final int DIC_USER_TYPED = 0;
-    public static final int DIC_MAIN = 1;
-    public static final int DIC_USER = 2;
-    public static final int DIC_USER_HISTORY = 3;
-    public static final int DIC_CONTACTS = 4;
-    public static final int DIC_WHITELIST = 6;
-    // If you add a type of dictionary, increment DIC_TYPE_LAST_ID
-    // TODO: this value seems unused. Remove it?
-    public static final int DIC_TYPE_LAST_ID = 6;
-    public static final String DICT_KEY_MAIN = "main";
-    public static final String DICT_KEY_CONTACTS = "contacts";
-    // User dictionary, the system-managed one.
-    public static final String DICT_KEY_USER = "user";
-    // User history dictionary for the unigram map, internal to LatinIME
-    public static final String DICT_KEY_USER_HISTORY_UNIGRAM = "history_unigram";
-    // User history dictionary for the bigram map, internal to LatinIME
-    public static final String DICT_KEY_USER_HISTORY_BIGRAM = "history_bigram";
-    public static final String DICT_KEY_WHITELIST ="whitelist";
+    public interface SuggestInitializationListener {
+        public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
+    }
 
     private static final boolean DBG = LatinImeLogger.sDBG;
 
-    private boolean mHasMainDictionary;
-    private Dictionary mContactsDict;
-    private WhitelistDictionary mWhiteListDictionary;
-    private final ConcurrentHashMap<String, Dictionary> mUnigramDictionaries =
-            new ConcurrentHashMap<String, Dictionary>();
-    private final ConcurrentHashMap<String, Dictionary> mBigramDictionaries =
-            new ConcurrentHashMap<String, Dictionary>();
+    private Dictionary mMainDictionary;
+    private ContactsBinaryDictionary mContactsDict;
+    private final ConcurrentHashMap<String, Dictionary> mDictionaries =
+            CollectionUtils.newConcurrentHashMap();
 
-    private int mPrefMaxSuggestions = 18;
-
-    private static final int PREF_MAX_BIGRAMS = 60;
+    public static final int MAX_SUGGESTIONS = 18;
 
     private float mAutoCorrectionThreshold;
 
-    private ArrayList<SuggestedWordInfo> mSuggestions = new ArrayList<SuggestedWordInfo>();
-    private ArrayList<SuggestedWordInfo> mBigramSuggestions = new ArrayList<SuggestedWordInfo>();
-    private CharSequence mConsideredWord;
+    // Locale used for upper- and title-casing words
+    private final Locale mLocale;
 
-    // TODO: Remove these member variables by passing more context to addWord() callback method
-    private boolean mIsFirstCharCapitalized;
-    private boolean mIsAllUpperCase;
-    private int mTrailingSingleQuotesCount;
-
-    private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
-
-    public Suggest(final Context context, final Locale locale) {
-        initAsynchronously(context, locale);
+    public Suggest(final Context context, final Locale locale,
+            final SuggestInitializationListener listener) {
+        initAsynchronously(context, locale, listener);
+        mLocale = locale;
     }
 
     /* package for test */ Suggest(final Context context, final File dictionary,
             final long startOffset, final long length, final Locale locale) {
         final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
                 startOffset, length /* useFullEditDistance */, false, locale);
-        mHasMainDictionary = null != mainDict;
-        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
-        addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
-        initWhitelistAndAutocorrectAndPool(context, locale);
+        mLocale = locale;
+        mMainDictionary = mainDict;
+        addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict);
     }
 
-    private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) {
-        mWhiteListDictionary = new WhitelistDictionary(context, locale);
-        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_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(
@@ -128,16 +96,22 @@
         }
     }
 
-    public void resetMainDict(final Context context, final Locale locale) {
-        mHasMainDictionary = false;
+    public void resetMainDict(final Context context, final Locale locale,
+            final SuggestInitializationListener listener) {
+        mMainDictionary = null;
+        if (listener != null) {
+            listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+        }
         new Thread("InitializeBinaryDictionary") {
             @Override
             public void run() {
                 final DictionaryCollection newMainDict =
                         DictionaryFactory.createMainDictionaryFromManager(context, locale);
-                mHasMainDictionary = null != newMainDict && !newMainDict.isEmpty();
-                addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
-                addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
+                addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, newMainDict);
+                mMainDictionary = newMainDict;
+                if (listener != null) {
+                    listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+                }
             }
         }.start();
     }
@@ -145,27 +119,27 @@
     // The main dictionary could have been loaded asynchronously.  Don't cache the return value
     // of this method.
     public boolean hasMainDictionary() {
-        return mHasMainDictionary;
+        return null != mMainDictionary && mMainDictionary.isInitialized();
     }
 
-    public Dictionary getContactsDictionary() {
+    public Dictionary getMainDictionary() {
+        return mMainDictionary;
+    }
+
+    public ContactsBinaryDictionary getContactsDictionary() {
         return mContactsDict;
     }
 
     public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
-        return mUnigramDictionaries;
-    }
-
-    public static int getApproxMaxWordLength() {
-        return APPROX_MAX_WORD_LENGTH;
+        return mDictionaries;
     }
 
     /**
      * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
      * before the main dictionary, if set. This refers to the system-managed user dictionary.
      */
-    public void setUserDictionary(Dictionary userDictionary) {
-        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary);
+    public void setUserDictionary(UserBinaryDictionary userDictionary) {
+        addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER, userDictionary);
     }
 
     /**
@@ -173,245 +147,202 @@
      * the contacts dictionary by passing null to this method. In this case no contacts dictionary
      * won't be used.
      */
-    public void setContactsDictionary(Dictionary contactsDictionary) {
+    public void setContactsDictionary(ContactsBinaryDictionary contactsDictionary) {
         mContactsDict = contactsDictionary;
-        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
-        addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
+        addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_CONTACTS, contactsDictionary);
     }
 
-    public void setUserHistoryDictionary(Dictionary userHistoryDictionary) {
-        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER_HISTORY_UNIGRAM,
-                userHistoryDictionary);
-        addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_HISTORY_BIGRAM,
-                userHistoryDictionary);
+    public void setUserHistoryDictionary(UserHistoryDictionary userHistoryDictionary) {
+        addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
     }
 
     public void setAutoCorrectionThreshold(float threshold) {
         mAutoCorrectionThreshold = threshold;
     }
 
-    private static CharSequence capitalizeWord(final boolean all, final boolean first,
-            final CharSequence word) {
-        if (TextUtils.isEmpty(word) || !(all || first)) return word;
-        final int wordLength = word.length();
-        final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
-        // TODO: Must pay attention to locale when changing case.
-        if (all) {
-            sb.append(word.toString().toUpperCase());
-        } else if (first) {
-            sb.append(Character.toUpperCase(word.charAt(0)));
-            if (wordLength > 1) {
-                sb.append(word.subSequence(1, wordLength));
-            }
-        }
-        return sb;
-    }
-
-    protected void addBigramToSuggestions(SuggestedWordInfo bigram) {
-        mSuggestions.add(bigram);
-    }
-
-    private static final WordComposer sEmptyWordComposer = new WordComposer();
-    public SuggestedWords getBigramPredictions(CharSequence prevWordForBigram) {
-        LatinImeLogger.onStartSuggestion(prevWordForBigram);
-        mIsFirstCharCapitalized = false;
-        mIsAllUpperCase = false;
-        mTrailingSingleQuotesCount = 0;
-        mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
-
-        // Treating USER_TYPED as UNIGRAM suggestion for logging now.
-        LatinImeLogger.onAddSuggestedWord("", Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
-        mConsideredWord = "";
-
-        mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
-
-        getAllBigrams(prevWordForBigram, sEmptyWordComposer);
-
-        // Nothing entered: return all bigrams for the previous word
-        int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
-        for (int i = 0; i < insertCount; ++i) {
-            addBigramToSuggestions(mBigramSuggestions.get(i));
-        }
-
-        SuggestedWordInfo.removeDups(mSuggestions);
-
-        return new SuggestedWords(mSuggestions,
-                false /* typedWordValid */,
-                false /* hasAutoCorrectionCandidate */,
-                false /* allowsToBeAutoCorrected */,
-                false /* isPunctuationSuggestions */,
-                false /* isObsoleteSuggestions */,
-                true /* isPrediction */);
-    }
-
-    // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
     public SuggestedWords getSuggestedWords(
             final WordComposer wordComposer, CharSequence prevWordForBigram,
-            final ProximityInfo proximityInfo, final int correctionMode) {
+            final ProximityInfo proximityInfo, final boolean isCorrectionEnabled, int sessionId) {
         LatinImeLogger.onStartSuggestion(prevWordForBigram);
-        mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
-        mIsAllUpperCase = wordComposer.isAllUpperCase();
-        mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
-        mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
+        if (wordComposer.isBatchMode()) {
+            return getSuggestedWordsForBatchInput(
+                    wordComposer, prevWordForBigram, proximityInfo, sessionId);
+        } else {
+            return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
+                    isCorrectionEnabled);
+        }
+    }
+
+    // Retrieves suggestions for the typing input.
+    private SuggestedWords getSuggestedWordsForTypingInput(
+            final WordComposer wordComposer, CharSequence prevWordForBigram,
+            final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) {
+        final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
+        final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
+                MAX_SUGGESTIONS);
 
         final String typedWord = wordComposer.getTypedWord();
-        final String consideredWord = mTrailingSingleQuotesCount > 0
-                ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount)
+        final String consideredWord = trailingSingleQuotesCount > 0
+                ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount)
                 : typedWord;
-        // Treating USER_TYPED as UNIGRAM suggestion for logging now.
-        LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
-        mConsideredWord = consideredWord;
+        LatinImeLogger.onAddSuggestedWord(typedWord, Dictionary.TYPE_USER_TYPED);
 
-        if (wordComposer.size() <= 1 && (correctionMode == CORRECTION_FULL_BIGRAM)) {
-            // At first character typed, search only the bigrams
-            mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
-
-            if (!TextUtils.isEmpty(prevWordForBigram)) {
-                getAllBigrams(prevWordForBigram, wordComposer);
-                if (TextUtils.isEmpty(consideredWord)) {
-                    // Nothing entered: return all bigrams for the previous word
-                    int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
-                    for (int i = 0; i < insertCount; ++i) {
-                        addBigramToSuggestions(mBigramSuggestions.get(i));
-                    }
-                } else {
-                    // Word entered: return only bigrams that match the first char of the typed word
-                    final char currentChar = consideredWord.charAt(0);
-                    // TODO: Must pay attention to locale when changing case.
-                    // TODO: Use codepoint instead of char
-                    final char currentCharUpper = Character.toUpperCase(currentChar);
-                    int count = 0;
-                    final int bigramSuggestionSize = mBigramSuggestions.size();
-                    for (int i = 0; i < bigramSuggestionSize; i++) {
-                        final SuggestedWordInfo bigramSuggestion = mBigramSuggestions.get(i);
-                        final char bigramSuggestionFirstChar =
-                                (char)bigramSuggestion.codePointAt(0);
-                        if (bigramSuggestionFirstChar == currentChar
-                                || bigramSuggestionFirstChar == currentCharUpper) {
-                            addBigramToSuggestions(bigramSuggestion);
-                            if (++count > mPrefMaxSuggestions) break;
-                        }
-                    }
-                }
+        final WordComposer wordComposerForLookup;
+        if (trailingSingleQuotesCount > 0) {
+            wordComposerForLookup = new WordComposer(wordComposer);
+            for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
+                wordComposerForLookup.deleteLast();
             }
-
-        } else if (wordComposer.size() > 1) {
-            final WordComposer wordComposerForLookup;
-            if (mTrailingSingleQuotesCount > 0) {
-                wordComposerForLookup = new WordComposer(wordComposer);
-                for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
-                    wordComposerForLookup.deleteLast();
-                }
-            } else {
-                wordComposerForLookup = wordComposer;
-            }
-            // At second character typed, search the unigrams (scores being affected by bigrams)
-            for (final String key : mUnigramDictionaries.keySet()) {
-                // Skip UserUnigramDictionary and WhitelistDictionary to lookup
-                if (key.equals(DICT_KEY_USER_HISTORY_UNIGRAM) || key.equals(DICT_KEY_WHITELIST))
-                    continue;
-                final Dictionary dictionary = mUnigramDictionaries.get(key);
-                dictionary.getWords(wordComposerForLookup, prevWordForBigram, this, proximityInfo);
-            }
+        } else {
+            wordComposerForLookup = wordComposer;
         }
 
-        final CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase,
-                mIsFirstCharCapitalized, mWhiteListDictionary.getWhitelistedWord(consideredWord));
+        for (final String key : mDictionaries.keySet()) {
+            final Dictionary dictionary = mDictionaries.get(key);
+            suggestionsSet.addAll(dictionary.getSuggestions(
+                    wordComposerForLookup, prevWordForBigram, proximityInfo));
+        }
+
+        final CharSequence whitelistedWord;
+        if (suggestionsSet.isEmpty()) {
+            whitelistedWord = null;
+        } else if (SuggestedWordInfo.KIND_WHITELIST != suggestionsSet.first().mKind) {
+            whitelistedWord = null;
+        } else {
+            whitelistedWord = suggestionsSet.first().mWord;
+        }
+
+        // The word can be auto-corrected if it has a whitelist entry that is not itself,
+        // or if it's a 2+ characters non-word (i.e. it's not in the dictionary).
+        final boolean allowsToBeAutoCorrected = (null != whitelistedWord
+                && !whitelistedWord.equals(consideredWord))
+                || (consideredWord.length() > 1 && !AutoCorrection.isInTheDictionary(mDictionaries,
+                        consideredWord, wordComposer.isFirstCharCapitalized()));
 
         final boolean hasAutoCorrection;
-        if (CORRECTION_FULL == correctionMode || CORRECTION_FULL_BIGRAM == correctionMode) {
-            final CharSequence autoCorrection =
-                    AutoCorrection.computeAutoCorrectionWord(mUnigramDictionaries, wordComposer,
-                            mSuggestions, consideredWord, mAutoCorrectionThreshold,
-                            whitelistedWord);
-            hasAutoCorrection = (null != autoCorrection);
-        } else {
+        // 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
+        // same time, it feels wrong that the SuggestedWord object includes information about
+        // the current settings. It may also be useful to know, when the setting is off, whether
+        // the word *would* have been auto-corrected.
+        if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord()
+                || suggestionsSet.isEmpty() || wordComposer.hasDigits()
+                || wordComposer.isMostlyCaps() || wordComposer.isResumed()
+                || !hasMainDictionary()) {
+            // If we don't have a main dictionary, we never want to auto-correct. The reason for
+            // this is, the user may have a contact whose name happens to match a valid word in
+            // their language, and it will unexpectedly auto-correct. For example, if the user
+            // types in English with no dictionary and has a "Will" in their contact list, "will"
+            // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no
+            // auto-correct.
             hasAutoCorrection = false;
+        } else {
+            hasAutoCorrection = AutoCorrection.suggestionExceedsAutoCorrectionThreshold(
+                    suggestionsSet.first(), consideredWord, mAutoCorrectionThreshold);
         }
 
-        if (whitelistedWord != null) {
-            if (mTrailingSingleQuotesCount > 0) {
-                final StringBuilder sb = new StringBuilder(whitelistedWord);
-                for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
-                    sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
-                }
-                mSuggestions.add(0, new SuggestedWordInfo(
-                        sb.toString(), SuggestedWordInfo.MAX_SCORE));
-            } else {
-                mSuggestions.add(0, new SuggestedWordInfo(
-                        whitelistedWord, SuggestedWordInfo.MAX_SCORE));
+        final ArrayList<SuggestedWordInfo> suggestionsContainer =
+                CollectionUtils.newArrayList(suggestionsSet);
+        final int suggestionsCount = suggestionsContainer.size();
+        final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
+        final boolean isAllUpperCase = wordComposer.isAllUpperCase();
+        if (isFirstCharCapitalized || isAllUpperCase || 0 != trailingSingleQuotesCount) {
+            for (int i = 0; i < suggestionsCount; ++i) {
+                final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+                final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
+                        wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized,
+                        trailingSingleQuotesCount);
+                suggestionsContainer.set(i, transformedWordInfo);
             }
         }
 
-        mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
-        SuggestedWordInfo.removeDups(mSuggestions);
+        for (int i = 0; i < suggestionsCount; ++i) {
+            final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+            LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict);
+        }
+
+        if (!TextUtils.isEmpty(typedWord)) {
+            suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
+                    SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
+                    Dictionary.TYPE_USER_TYPED));
+        }
+        SuggestedWordInfo.removeDups(suggestionsContainer);
 
         final ArrayList<SuggestedWordInfo> suggestionsList;
-        if (DBG) {
-            suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, mSuggestions);
+        if (DBG && !suggestionsContainer.isEmpty()) {
+            suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, suggestionsContainer);
         } else {
-            suggestionsList = mSuggestions;
+            suggestionsList = suggestionsContainer;
         }
 
-        // 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.allowsToBeAutoCorrected(
-                getUnigramDictionaries(), consideredWord, wordComposer.isFirstCharCapitalized())
-        // If we don't have a main dictionary, we never want to auto-correct. The reason for this
-        // is, the user may have a contact whose name happens to match a valid word in their
-        // language, and it will unexpectedly auto-correct. For example, if the user types in
-        // English with no dictionary and has a "Will" in their contact list, "will" would
-        // always auto-correct to "Will" which is unwanted. Hence, no main dict => no auto-correct.
-                && mHasMainDictionary;
-
-        boolean autoCorrectionAvailable = hasAutoCorrection;
-        if (correctionMode == CORRECTION_FULL || correctionMode == CORRECTION_FULL_BIGRAM) {
-            autoCorrectionAvailable |= !allowsToBeAutoCorrected;
-        }
-        // Don't auto-correct words with multiple capital letter
-        autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
-        autoCorrectionAvailable &= !wordComposer.isResumed();
-        if (allowsToBeAutoCorrected && suggestionsList.size() > 1 && mAutoCorrectionThreshold > 0
-                && Suggest.shouldBlockAutoCorrectionBySafetyNet(typedWord,
-                        suggestionsList.get(1).mWord)) {
-            autoCorrectionAvailable = false;
-        }
         return new SuggestedWords(suggestionsList,
+                // TODO: this first argument is lying. If this is a whitelisted word which is an
+                // actual word, it says typedWordValid = false, which looks wrong. We should either
+                // rename the attribute or change the value.
                 !allowsToBeAutoCorrected /* typedWordValid */,
-                autoCorrectionAvailable /* hasAutoCorrectionCandidate */,
-                allowsToBeAutoCorrected /* allowsToBeAutoCorrected */,
+                hasAutoCorrection, /* willAutoCorrect */
+                false /* isPunctuationSuggestions */,
+                false /* isObsoleteSuggestions */,
+                !wordComposer.isComposingWord() /* isPrediction */);
+    }
+
+    // Retrieves suggestions for the batch input.
+    private SuggestedWords getSuggestedWordsForBatchInput(
+            final WordComposer wordComposer, CharSequence prevWordForBigram,
+            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 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.getSuggestionsWithSessionId(
+                    wordComposer, prevWordForBigram, proximityInfo, sessionId));
+        }
+
+        for (SuggestedWordInfo wordInfo : suggestionsSet) {
+            LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict);
+        }
+
+        final ArrayList<SuggestedWordInfo> suggestionsContainer =
+                CollectionUtils.newArrayList(suggestionsSet);
+        final int suggestionsCount = suggestionsContainer.size();
+        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);
+                final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
+                        wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized,
+                        0 /* trailingSingleQuotesCount */);
+                suggestionsContainer.set(i, transformedWordInfo);
+            }
+        }
+
+        SuggestedWordInfo.removeDups(suggestionsContainer);
+        // In the batch input mode, the most relevant suggested word should act as a "typed word"
+        // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
+        return new SuggestedWords(suggestionsContainer,
+                true /* typedWordValid */,
+                false /* willAutoCorrect */,
                 false /* isPunctuationSuggestions */,
                 false /* isObsoleteSuggestions */,
                 false /* isPrediction */);
     }
 
-    /**
-     * Adds all bigram predictions for prevWord. Also checks the lower case version of prevWord if
-     * it contains any upper case characters.
-     */
-    private void getAllBigrams(final CharSequence prevWord, final WordComposer wordComposer) {
-        if (StringUtils.hasUpperCase(prevWord)) {
-            // TODO: Must pay attention to locale when changing case.
-            final CharSequence lowerPrevWord = prevWord.toString().toLowerCase();
-            for (final Dictionary dictionary : mBigramDictionaries.values()) {
-                dictionary.getBigrams(wordComposer, lowerPrevWord, this);
-            }
-        }
-        for (final Dictionary dictionary : mBigramDictionaries.values()) {
-            dictionary.getBigrams(wordComposer, prevWord, this);
-        }
-    }
-
     private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
             final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) {
         final SuggestedWordInfo typedWordInfo = suggestions.get(0);
         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.
@@ -431,119 +362,44 @@
         return suggestionsList;
     }
 
-    // TODO: Use codepoint instead of char
-    @Override
-    public boolean addWord(final char[] word, final int offset, final int length, int score,
-            final int dicTypeId, final int dataType) {
-        int dataTypeForLog = dataType;
-        final ArrayList<SuggestedWordInfo> suggestions;
-        final int prefMaxSuggestions;
-        if (dataType == Dictionary.BIGRAM) {
-            suggestions = mBigramSuggestions;
-            prefMaxSuggestions = PREF_MAX_BIGRAMS;
-        } else {
-            suggestions = mSuggestions;
-            prefMaxSuggestions = mPrefMaxSuggestions;
+    private static class SuggestedWordInfoComparator implements Comparator<SuggestedWordInfo> {
+        // This comparator ranks the word info with the higher frequency first. That's because
+        // that's the order we want our elements in.
+        @Override
+        public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) {
+            if (o1.mScore > o2.mScore) return -1;
+            if (o1.mScore < o2.mScore) return 1;
+            if (o1.mCodePointCount < o2.mCodePointCount) return -1;
+            if (o1.mCodePointCount > o2.mCodePointCount) return 1;
+            return o1.mWord.toString().compareTo(o2.mWord.toString());
         }
+    }
+    private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator =
+            new SuggestedWordInfoComparator();
 
-        int pos = 0;
-
-        // Check if it's the same word, only caps are different
-        if (StringUtils.equalsIgnoreCase(mConsideredWord, word, offset, length)) {
-            // TODO: remove this surrounding if clause and move this logic to
-            // getSuggestedWordBuilder.
-            if (suggestions.size() > 0) {
-                final SuggestedWordInfo currentHighestWord = suggestions.get(0);
-                // If the current highest word is also equal to typed word, we need to compare
-                // frequency to determine the insertion position. This does not ensure strictly
-                // correct ordering, but ensures the top score is on top which is enough for
-                // removing duplicates correctly.
-                if (StringUtils.equalsIgnoreCase(currentHighestWord.mWord, word, offset, length)
-                        && score <= currentHighestWord.mScore) {
-                    pos = 1;
-                }
-            }
+    private static SuggestedWordInfo getTransformedSuggestedWordInfo(
+            final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
+            final boolean isFirstCharCapitalized, final int trailingSingleQuotesCount) {
+        final StringBuilder sb = new StringBuilder(wordInfo.mWord.length());
+        if (isAllUpperCase) {
+            sb.append(wordInfo.mWord.toString().toUpperCase(locale));
+        } else if (isFirstCharCapitalized) {
+            sb.append(StringUtils.toTitleCase(wordInfo.mWord.toString(), locale));
         } else {
-            // Check the last one's score and bail
-            if (suggestions.size() >= prefMaxSuggestions
-                    && suggestions.get(prefMaxSuggestions - 1).mScore >= score) return true;
-            while (pos < suggestions.size()) {
-                final int curScore = suggestions.get(pos).mScore;
-                if (curScore < score
-                        || (curScore == score && length < suggestions.get(pos).codePointCount())) {
-                    break;
-                }
-                pos++;
-            }
+            sb.append(wordInfo.mWord);
         }
-        if (pos >= prefMaxSuggestions) {
-            return true;
-        }
-
-        final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
-        // TODO: Must pay attention to locale when changing case.
-        if (mIsAllUpperCase) {
-            sb.append(new String(word, offset, length).toUpperCase());
-        } else if (mIsFirstCharCapitalized) {
-            sb.append(Character.toUpperCase(word[offset]));
-            if (length > 1) {
-                sb.append(word, offset + 1, length - 1);
-            }
-        } else {
-            sb.append(word, offset, length);
-        }
-        for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
+        for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
             sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
         }
-        suggestions.add(pos, new SuggestedWordInfo(sb, score));
-        if (suggestions.size() > prefMaxSuggestions) {
-            suggestions.remove(prefMaxSuggestions);
-        } else {
-            LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
-        }
-        return true;
+        return new SuggestedWordInfo(sb, wordInfo.mScore, wordInfo.mKind, wordInfo.mSourceDict);
     }
 
     public void close() {
-        final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>();
-        dictionaries.addAll(mUnigramDictionaries.values());
-        dictionaries.addAll(mBigramDictionaries.values());
+        final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
+        dictionaries.addAll(mDictionaries.values());
         for (final Dictionary dictionary : dictionaries) {
             dictionary.close();
         }
-        mHasMainDictionary = false;
-    }
-
-    // TODO: Resolve the inconsistencies between the native auto correction algorithms and
-    // this safety net
-    public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
-            final CharSequence suggestion) {
-        // Safety net for auto correction.
-        // Actually if we hit this safety net, it's a bug.
-        // If user selected aggressive auto correction mode, there is no need to use the safety
-        // net.
-        // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
-        // we should not use net because relatively edit distance can be big.
-        final int typedWordLength = typedWord.length();
-        if (typedWordLength < Suggest.MINIMUM_SAFETY_NET_CHAR_LENGTH) {
-            return false;
-        }
-        final int maxEditDistanceOfNativeDictionary =
-                (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
-        final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
-        if (DBG) {
-            Log.d(TAG, "Autocorrected edit distance = " + distance
-                    + ", " + maxEditDistanceOfNativeDictionary);
-        }
-        if (distance > maxEditDistanceOfNativeDictionary) {
-            if (DBG) {
-                Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
-                Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
-                        + "Turning off auto-correction.");
-            }
-            return true;
-        } else {
-            return false;
-        }
+        mMainDictionary = null;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 497fd3b..d9f48c4 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -24,28 +24,30 @@
 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, false);
+            EMPTY_WORD_INFO_LIST, false, false, false, false, false);
 
     public final boolean mTypedWordValid;
-    public final boolean mHasAutoCorrectionCandidate;
+    // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition
+    // of what this flag means would be "the top suggestion is strong enough to auto-correct",
+    // whether this exactly matches the user entry or not.
+    public final boolean mWillAutoCorrect;
     public final boolean mIsPunctuationSuggestions;
-    public final boolean mAllowsToBeAutoCorrected;
     public final boolean mIsObsoleteSuggestions;
     public final boolean mIsPrediction;
     private final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList;
 
     public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
             final boolean typedWordValid,
-            final boolean hasAutoCorrectionCandidate,
-            final boolean allowsToBeAutoCorrected,
+            final boolean willAutoCorrect,
             final boolean isPunctuationSuggestions,
             final boolean isObsoleteSuggestions,
             final boolean isPrediction) {
         mSuggestedWordInfoList = suggestedWordInfoList;
         mTypedWordValid = typedWordValid;
-        mHasAutoCorrectionCandidate = hasAutoCorrectionCandidate;
-        mAllowsToBeAutoCorrected = allowsToBeAutoCorrected;
+        mWillAutoCorrect = willAutoCorrect;
         mIsPunctuationSuggestions = isPunctuationSuggestions;
         mIsObsoleteSuggestions = isObsoleteSuggestions;
         mIsPrediction = isPrediction;
@@ -55,7 +57,7 @@
         return mSuggestedWordInfoList.size();
     }
 
-    public CharSequence getWord(int pos) {
+    public String getWord(int pos) {
         return mSuggestedWordInfoList.get(pos).mWord;
     }
 
@@ -67,12 +69,8 @@
         return mSuggestedWordInfoList.get(pos);
     }
 
-    public boolean hasAutoCorrectionWord() {
-        return mHasAutoCorrectionCandidate && size() > 1 && !mTypedWordValid;
-    }
-
     public boolean willAutoCorrect() {
-        return !mTypedWordValid && mHasAutoCorrectionCandidate;
+        return mWillAutoCorrect;
     }
 
     @Override
@@ -80,18 +78,18 @@
         // Pretty-print method to help debug
         return "SuggestedWords:"
                 + " mTypedWordValid=" + mTypedWordValid
-                + " mHasAutoCorrectionCandidate=" + mHasAutoCorrectionCandidate
-                + " mAllowsToBeAutoCorrected=" + mAllowsToBeAutoCorrected
+                + " mWillAutoCorrect=" + mWillAutoCorrect
                 + " mIsPunctuationSuggestions=" + mIsPunctuationSuggestions
                 + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray());
     }
 
     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));
+                result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE,
+                        SuggestedWordInfo.KIND_APP_DEFINED, Dictionary.TYPE_APPLICATION_DEFINED));
             }
         }
         return result;
@@ -101,9 +99,10 @@
     // 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>();
-        suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
+        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());
         final int previousSize = previousSuggestions.size();
         for (int pos = 1; pos < previousSize; pos++) {
@@ -120,17 +119,29 @@
 
     public static class SuggestedWordInfo {
         public static final int MAX_SCORE = Integer.MAX_VALUE;
-        private final String mWordStr;
-        public final CharSequence mWord;
+        public static final int KIND_TYPED = 0; // What user typed
+        public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
+        public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
+        public static final int KIND_WHITELIST = 3; // Whitelisted word
+        public static final int KIND_BLACKLIST = 4; // Blacklisted word
+        public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
+        public static final int KIND_APP_DEFINED = 6; // Suggested by the application
+        public static final int KIND_SHORTCUT = 7; // A shortcut
+        public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
+        public final String mWord;
         public final int mScore;
+        public final int mKind; // one of the KIND_* constants above
         public final int mCodePointCount;
+        public final String mSourceDict;
         private String mDebugString = "";
 
-        public SuggestedWordInfo(final CharSequence word, final int score) {
-            mWordStr = word.toString();
-            mWord = word;
+        public SuggestedWordInfo(final CharSequence word, final int score, final int kind,
+                final String sourceDict) {
+            mWord = word.toString();
             mScore = score;
-            mCodePointCount = mWordStr.codePointCount(0, mWordStr.length());
+            mKind = kind;
+            mSourceDict = sourceDict;
+            mCodePointCount = StringUtils.codePointCount(mWord);
         }
 
 
@@ -148,15 +159,15 @@
         }
 
         public int codePointAt(int i) {
-            return mWordStr.codePointAt(i);
+            return mWord.codePointAt(i);
         }
 
         @Override
         public String toString() {
             if (TextUtils.isEmpty(mDebugString)) {
-                return mWordStr;
+                return mWord;
             } else {
-                return mWordStr + " (" + mDebugString.toString() + ")";
+                return mWord + " (" + mDebugString.toString() + ")";
             }
         }
 
@@ -166,11 +177,11 @@
                 return;
             }
             int i = 1;
-            while(i < candidates.size()) {
+            while (i < candidates.size()) {
                 final SuggestedWordInfo cur = candidates.get(i);
                 for (int j = 0; j < i; ++j) {
                     final SuggestedWordInfo previous = candidates.get(j);
-                    if (TextUtils.equals(cur.mWord, previous.mWord)) {
+                    if (cur.mWord.equals(previous.mWord)) {
                         candidates.remove(cur.mScore < previous.mScore ? i : j);
                         --i;
                         break;
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index 673b545..bdd988d 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -19,22 +19,23 @@
 import android.content.Context;
 
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 
+import java.util.ArrayList;
 import java.util.Locale;
 
 public class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary {
     private boolean mClosed;
 
     public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) {
-        super(context, Suggest.DIC_CONTACTS, locale);
+        super(context, locale);
     }
 
     @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
+    public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+            final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
         syncReloadDictionaryIfRequired();
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+        return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
deleted file mode 100644
index a8b871c..0000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedContactsDictionary extends ContactsDictionary {
-    private boolean mClosed;
-
-    public SynchronouslyLoadedContactsDictionary(final Context context) {
-        super(context, Suggest.DIC_CONTACTS);
-        mClosed = false;
-    }
-
-    @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
-        blockingReloadDictionaryIfRequired();
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    @Override
-    public synchronized boolean isValidWord(CharSequence word) {
-        blockingReloadDictionaryIfRequired();
-        return getWordFrequency(word) > -1;
-    }
-
-    // Protect against multiple closing
-    @Override
-    public synchronized void close() {
-        // Actually with the current implementation of ContactsDictionary it's safe to close
-        // several times, so the following protection is really only for foolproofing
-        if (mClosed) return;
-        mClosed = true;
-        super.close();
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 1606a34..b8cfddd 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -19,6 +19,9 @@
 import android.content.Context;
 
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.ArrayList;
 
 public class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary {
 
@@ -32,11 +35,10 @@
     }
 
     @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
+    public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+            final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
         syncReloadDictionaryIfRequired();
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+        return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
deleted file mode 100644
index 23a49c1..0000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedUserDictionary extends UserDictionary {
-    private boolean mClosed;
-
-    public SynchronouslyLoadedUserDictionary(final Context context, final String locale) {
-        this(context, locale, false);
-    }
-
-    public SynchronouslyLoadedUserDictionary(final Context context, final String locale,
-            final boolean alsoUseMoreRestrictiveLocales) {
-        super(context, locale, alsoUseMoreRestrictiveLocales);
-    }
-
-    @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
-        blockingReloadDictionaryIfRequired();
-        getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    @Override
-    public synchronized boolean isValidWord(CharSequence word) {
-        blockingReloadDictionaryIfRequired();
-        return super.isValidWord(word);
-    }
-
-    // Protect against multiple closing
-    @Override
-    public synchronized void close() {
-        if (mClosed) return;
-        mClosed = true;
-        super.close();
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 6fa1a25..60e6fa1 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -34,13 +34,27 @@
  */
 public class UserBinaryDictionary extends ExpandableBinaryDictionary {
 
-    // TODO: use Words.SHORTCUT when it's public in the SDK
+    // The user dictionary provider uses an empty string to mean "all languages".
+    private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
+
+    // TODO: use Words.SHORTCUT when we target JellyBean or above
     final static String SHORTCUT = "shortcut";
-    private static final String[] PROJECTION_QUERY = {
-        Words.WORD,
-        SHORTCUT,
-        Words.FREQUENCY,
-    };
+    private static final String[] PROJECTION_QUERY;
+    static {
+        // 16 is JellyBean, but we want this to compile against ICS.
+        if (android.os.Build.VERSION.SDK_INT >= 16) {
+            PROJECTION_QUERY = new String[] {
+                Words.WORD,
+                SHORTCUT,
+                Words.FREQUENCY,
+            };
+        } else {
+            PROJECTION_QUERY = new String[] {
+                Words.WORD,
+                Words.FREQUENCY,
+            };
+        }
+    }
 
     private static final String NAME = "userunigram";
 
@@ -58,9 +72,14 @@
 
     public UserBinaryDictionary(final Context context, final String locale,
             final boolean alsoUseMoreRestrictiveLocales) {
-        super(context, getFilenameWithLocale(NAME, locale), Suggest.DIC_USER);
+        super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER);
         if (null == locale) throw new NullPointerException(); // Catch the error earlier
-        mLocale = locale;
+        if (SubtypeLocale.NO_LANGUAGE.equals(locale)) {
+            // If we don't have a locale, insert into the "all locales" user dictionary.
+            mLocale = USER_DICTIONARY_ALL_LANGUAGES;
+        } else {
+            mLocale = locale;
+        }
         mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
         // Perform a managed query. The Activity will handle closing and re-querying the cursor
         // when needed.
@@ -136,7 +155,7 @@
             requestArguments = localeElements;
         }
         final Cursor cursor = mContext.getContentResolver().query(
-            Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
+                Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
         try {
             addWords(cursor);
         } finally {
@@ -182,16 +201,18 @@
     }
 
     private void addWords(Cursor cursor) {
+        // 16 is JellyBean, but we want this to compile against ICS.
+        final boolean hasShortcutColumn = android.os.Build.VERSION.SDK_INT >= 16;
         clearFusionDictionary();
         if (cursor == null) return;
         if (cursor.moveToFirst()) {
             final int indexWord = cursor.getColumnIndex(Words.WORD);
-            final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
+            final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(SHORTCUT) : 0;
             final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
             while (!cursor.isAfterLast()) {
-                String word = cursor.getString(indexWord);
-                String shortcut = cursor.getString(indexShortcut);
-                int frequency = cursor.getInt(indexFrequency);
+                final String word = cursor.getString(indexWord);
+                final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null;
+                final int frequency = cursor.getInt(indexFrequency);
                 // Safeguard against adding really long words.
                 if (word.length() < MAX_WORD_LENGTH) {
                     super.addWord(word, null, frequency);
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
deleted file mode 100644
index c1efadd..0000000
--- a/java/src/com/android/inputmethod/latin/UserDictionary.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.provider.UserDictionary.Words;
-import android.text.TextUtils;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-import java.util.Arrays;
-
-// TODO: This class is superseded by {@link UserBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words in the user unigram dictionary.
- *
- * @deprecated Use {@link UserBinaryDictionary}.
- */
-@Deprecated
-public class UserDictionary extends ExpandableDictionary {
-
-    // TODO: use Words.SHORTCUT when it's public in the SDK
-    final static String SHORTCUT = "shortcut";
-    private static final String[] PROJECTION_QUERY = {
-        Words.WORD,
-        SHORTCUT,
-        Words.FREQUENCY,
-    };
-
-    // This is not exported by the framework so we pretty much have to write it here verbatim
-    private static final String ACTION_USER_DICTIONARY_INSERT =
-            "com.android.settings.USER_DICTIONARY_INSERT";
-
-    private ContentObserver mObserver;
-    final private String mLocale;
-    final private boolean mAlsoUseMoreRestrictiveLocales;
-
-    public UserDictionary(final Context context, final String locale) {
-        this(context, locale, false);
-    }
-
-    public UserDictionary(final Context context, final String locale,
-            final boolean alsoUseMoreRestrictiveLocales) {
-        super(context, Suggest.DIC_USER);
-        if (null == locale) throw new NullPointerException(); // Catch the error earlier
-        mLocale = locale;
-        mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
-        // Perform a managed query. The Activity will handle closing and re-querying the cursor
-        // when needed.
-        ContentResolver cres = context.getContentResolver();
-
-        mObserver = new ContentObserver(null) {
-            @Override
-            public void onChange(boolean self) {
-                setRequiresReload(true);
-            }
-        };
-        cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
-
-        loadDictionary();
-    }
-
-    @Override
-    public synchronized void close() {
-        if (mObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-            mObserver = null;
-        }
-        super.close();
-    }
-
-    @Override
-    public void loadDictionaryAsync() {
-        // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
-        // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
-        // This is correct for locale processing.
-        // For this example, we'll look at the "en_US_POSIX" case.
-        final String[] localeElements =
-                TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3);
-        final int length = localeElements.length;
-
-        final StringBuilder request = new StringBuilder("(locale is NULL)");
-        String localeSoFar = "";
-        // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
-        // and request = "(locale is NULL)"
-        for (int i = 0; i < length; ++i) {
-            // i | localeSoFar    | localeElements
-            // 0 | ""             | ["en", "US", "POSIX"]
-            // 1 | "en_"          | ["en", "US", "POSIX"]
-            // 2 | "en_US_"       | ["en", "en_US", "POSIX"]
-            localeElements[i] = localeSoFar + localeElements[i];
-            localeSoFar = localeElements[i] + "_";
-            // i | request
-            // 0 | "(locale is NULL)"
-            // 1 | "(locale is NULL) or (locale=?)"
-            // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
-            request.append(" or (locale=?)");
-        }
-        // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
-        // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
-
-        final String[] requestArguments;
-        // If length == 3, we already have all the arguments we need (common prefix is meaningless
-        // inside variants
-        if (mAlsoUseMoreRestrictiveLocales && length < 3) {
-            request.append(" or (locale like ?)");
-            // The following creates an array with one more (null) position
-            final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
-                    Arrays.copyOf(localeElements, length + 1);
-            localeElementsWithMoreRestrictiveLocalesIncluded[length] =
-                    localeElements[length - 1] + "_%";
-            requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
-            // If for example localeElements = ["en"]
-            // then requestArguments = ["en", "en_%"]
-            // and request = (locale is NULL) or (locale=?) or (locale like ?)
-            // If localeElements = ["en", "en_US"]
-            // then requestArguments = ["en", "en_US", "en_US_%"]
-        } else {
-            requestArguments = localeElements;
-        }
-        final Cursor cursor = getContext().getContentResolver()
-                .query(Words.CONTENT_URI, PROJECTION_QUERY, request.toString(),
-                        requestArguments, null);
-        try {
-            addWords(cursor);
-        } finally {
-            if (null != cursor) cursor.close();
-        }
-    }
-
-    public boolean isEnabled() {
-        final ContentResolver cr = getContext().getContentResolver();
-        final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
-        if (client != null) {
-            client.release();
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Adds a word to the user dictionary and makes it persistent.
-     *
-     * This will call upon the system interface to do the actual work through the intent
-     * readied by the system to this effect.
-     *
-     * @param word the word to add. If the word is capitalized, then the dictionary will
-     * recognize it as a capitalized word when searched.
-     * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
-     * the highest.
-     * @TODO use a higher or float range for frequency
-     */
-    public synchronized void addWordToUserDictionary(final String word, final int frequency) {
-        // Force load the dictionary here synchronously
-        if (getRequiresReload()) loadDictionaryAsync();
-        // TODO: do something for the UI. With the following, any sufficiently long word will
-        // look like it will go to the user dictionary but it won't.
-        // Safeguard against adding long words. Can cause stack overflow.
-        if (word.length() >= getMaxWordLength()) return;
-
-        // TODO: Add an argument to the intent to specify the frequency.
-        Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT);
-        intent.putExtra(Words.WORD, word);
-        intent.putExtra(Words.LOCALE, mLocale);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        getContext().startActivity(intent);
-    }
-
-    @Override
-    public synchronized void getWords(final WordComposer codes,
-            final CharSequence prevWordForBigrams, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
-        super.getWords(codes, prevWordForBigrams, callback, proximityInfo);
-    }
-
-    @Override
-    public synchronized boolean isValidWord(CharSequence word) {
-        return super.isValidWord(word);
-    }
-
-    private void addWords(Cursor cursor) {
-        clearDictionary();
-        if (cursor == null) return;
-        final int maxWordLength = getMaxWordLength();
-        if (cursor.moveToFirst()) {
-            final int indexWord = cursor.getColumnIndex(Words.WORD);
-            final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
-            final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
-            while (!cursor.isAfterLast()) {
-                String word = cursor.getString(indexWord);
-                String shortcut = cursor.getString(indexShortcut);
-                int frequency = cursor.getInt(indexFrequency);
-                // Safeguard against adding really long words. Stack may overflow due
-                // to recursion
-                if (word.length() < maxWordLength) {
-                    super.addWord(word, null, frequency);
-                }
-                if (null != shortcut && shortcut.length() < maxWordLength) {
-                    super.addWord(shortcut, word, frequency);
-                }
-                cursor.moveToNext();
-            }
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java
new file mode 100644
index 0000000..4a3d11a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java
@@ -0,0 +1,205 @@
+/*
+ * 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.Log;
+
+import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.PendingAttribute;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Reads and writes Binary files for a UserHistoryDictionary.
+ *
+ * All the methods in this class are static.
+ */
+public class UserHistoryDictIOUtils {
+    private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    public interface OnAddWordListener {
+        public void setUnigram(final String word, final String shortcutTarget, final int frequency);
+        public void setBigram(final String word1, final String word2, final int frequency);
+    }
+
+    public interface BigramDictionaryInterface {
+        public int getFrequency(final String word1, final String word2);
+    }
+
+    public static final class ByteArrayWrapper implements FusionDictionaryBufferInterface {
+        private byte[] mBuffer;
+        private int mPosition;
+
+        public ByteArrayWrapper(final byte[] buffer) {
+            mBuffer = buffer;
+            mPosition = 0;
+        }
+
+        @Override
+        public int readUnsignedByte() {
+            return ((int)mBuffer[mPosition++]) & 0xFF;
+        }
+
+        @Override
+        public int readUnsignedShort() {
+            final int retval = readUnsignedByte();
+            return (retval << 8) + readUnsignedByte();
+        }
+
+        @Override
+        public int readUnsignedInt24() {
+            final int retval = readUnsignedShort();
+            return (retval << 8) + readUnsignedByte();
+        }
+
+        @Override
+        public int readInt() {
+            final int retval = readUnsignedShort();
+            return (retval << 16) + readUnsignedShort();
+        }
+
+        @Override
+        public int position() {
+            return mPosition;
+        }
+
+        @Override
+        public void position(int position) {
+            mPosition = position;
+        }
+
+        @Override
+        public void put(final byte b) {
+            mBuffer[mPosition++] = b;
+        }
+
+        @Override
+        public int limit() {
+            return mBuffer.length;
+        }
+    }
+
+    /**
+     * Writes dictionary to file.
+     */
+    public static void writeDictionaryBinary(final OutputStream destination,
+            final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams,
+            final FormatOptions formatOptions) {
+
+        final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams);
+
+        try {
+            BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, formatOptions);
+        } catch (IOException e) {
+            Log.e(TAG, "IO exception while writing file: " + e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "Unsupported fomat: " + e);
+        }
+    }
+
+    /**
+     * Constructs a new FusionDictionary from BigramDictionaryInterface.
+     */
+    /* packages for test */ static FusionDictionary constructFusionDictionary(
+            final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) {
+
+        final FusionDictionary fusionDict = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(
+                        new HashMap<String,String>(), false, false));
+
+        for (final String word1 : bigrams.keySet()) {
+            final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1);
+            for (final String word2 : word1Bigrams.keySet()) {
+                final int freq = dict.getFrequency(word1, word2);
+
+                if (DEBUG) {
+                    if (word1 == null) {
+                        Log.d(TAG, "add unigram: " + word2 + "," + Integer.toString(freq));
+                    } else {
+                        Log.d(TAG, "add bigram: " + word1
+                                + "," + word2 + "," + Integer.toString(freq));
+                    }
+                }
+
+                if (word1 == null) { // unigram
+                    fusionDict.add(word2, freq, null, false /* isNotAWord */);
+                } else { // bigram
+                    fusionDict.setBigram(word1, word2, freq);
+                }
+                bigrams.updateBigram(word1, word2, (byte)freq);
+            }
+        }
+
+        return fusionDict;
+    }
+
+    /**
+     * Reads dictionary from file.
+     */
+    public static void readDictionaryBinary(final FusionDictionaryBufferInterface buffer,
+            final OnAddWordListener dict) {
+        final Map<Integer, String> unigrams = CollectionUtils.newTreeMap();
+        final Map<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
+        final Map<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
+
+        try {
+            BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies,
+                    bigrams);
+            addWordsFromWordMap(unigrams, frequencies, bigrams, dict);
+        } catch (IOException e) {
+            Log.e(TAG, "IO exception while reading file: " + e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "Unsupported format: " + e);
+        }
+    }
+
+    /**
+     * Adds all unigrams and bigrams in maps to OnAddWordListener.
+     */
+    /* package for test */ static void addWordsFromWordMap(final Map<Integer, String> unigrams,
+            final Map<Integer, Integer> frequencies,
+            final Map<Integer, ArrayList<PendingAttribute>> bigrams, final OnAddWordListener to) {
+
+        for (Map.Entry<Integer, String> entry : unigrams.entrySet()) {
+            final String word1 = entry.getValue();
+            final int unigramFrequency = frequencies.get(entry.getKey());
+            to.setUnigram(word1, null, unigramFrequency);
+
+            final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey());
+
+            if (attrList != null) {
+                for (final PendingAttribute attr : attrList) {
+                    to.setBigram(word1, unigrams.get(attr.mAddress),
+                            BinaryDictInputOutput.reconstructBigramFrequency(unigramFrequency,
+                                    attr.mFrequency));
+                }
+            }
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
index 5095f65..e03af64 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
@@ -27,9 +27,12 @@
 import android.provider.BaseColumns;
 import android.util.Log;
 
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
 
 import java.lang.ref.SoftReference;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.locks.ReentrantLock;
@@ -39,7 +42,7 @@
  * cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
  */
 public class UserHistoryDictionary extends ExpandableDictionary {
-    private static final String TAG = "UserHistoryDictionary";
+    private static final String TAG = UserHistoryDictionary.class.getSimpleName();
     public static final boolean DBG_SAVE_RESTORE = false;
     public static final boolean DBG_STRESS_TEST = false;
     public static final boolean DBG_ALWAYS_WRITE = false;
@@ -49,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
@@ -90,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);
@@ -106,17 +109,12 @@
 
     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(
-            final Context context, final String locale,
-            final int dictTypeId, final SharedPreferences sp) {
+            final Context context, final String locale, final SharedPreferences sp) {
         if (sLangDictCache.containsKey(locale)) {
             final SoftReference<UserHistoryDictionary> ref = sLangDictCache.get(locale);
             final UserHistoryDictionary dict = ref == null ? null : ref.get();
@@ -128,14 +126,14 @@
             }
         }
         final UserHistoryDictionary dict =
-                new UserHistoryDictionary(context, locale, dictTypeId, sp);
+                new UserHistoryDictionary(context, locale, sp);
         sLangDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict));
         return dict;
     }
 
-    private UserHistoryDictionary(final Context context, final String locale, final int dicTypeId,
-            SharedPreferences sp) {
-        super(context, dicTypeId);
+    private UserHistoryDictionary(final Context context, final String locale,
+            final SharedPreferences sp) {
+        super(context, Dictionary.TYPE_USER_HISTORY);
         mLocale = locale;
         mPrefs = sp;
         if (sOpenHelper == null) {
@@ -158,6 +156,14 @@
         // super.close();
     }
 
+    @Override
+    protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer composer,
+            final CharSequence prevWord, final ProximityInfo proximityInfo) {
+        // Inhibit suggestions (not predictions) for user history for now. Removing this method
+        // is enough to use it through the standard ExpandableDictionary way.
+        return null;
+    }
+
     /**
      * Return whether the passed charsequence is in the dictionary.
      */
@@ -176,6 +182,10 @@
      * The second word may not be null (a NullPointerException would be thrown).
      */
     public int addToUserHistory(final String word1, String word2, boolean isValid) {
+        if (word2.length() >= BinaryDictionary.MAX_WORD_LENGTH ||
+                (word1 != null && word1.length() >= BinaryDictionary.MAX_WORD_LENGTH)) {
+            return -1;
+        }
         if (mBigramListLock.tryLock()) {
             try {
                 super.addWord(
@@ -492,9 +502,11 @@
                                     needsToSave(fc, isValid, addLevel0Bigram)) {
                                 freq = fc;
                             } else {
+                                // Delete this entry
                                 freq = -1;
                             }
                         } else {
+                            // Delete this entry
                             freq = -1;
                         }
                     }
@@ -531,6 +543,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 2884774..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)) {
@@ -98,11 +97,11 @@
     }
 
     public HashMap<String, Byte> getBigrams(String word1) {
-        if (!mBigramMap.containsKey(word1)) {
-            return EMPTY_BIGRAM_MAP;
-        } else {
-            return mBigramMap.get(word1);
-        }
+        if (mBigramMap.containsKey(word1)) return mBigramMap.get(word1);
+        // TODO: lower case according to locale
+        final String lowerWord1 = word1.toLowerCase();
+        if (mBigramMap.containsKey(lowerWord1)) return mBigramMap.get(lowerWord1);
+        return EMPTY_BIGRAM_MAP;
     }
 
     public boolean removeBigram(String word1, String word2) {
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
index e5516dc..3d3bd98 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
@@ -19,7 +19,7 @@
 import android.text.format.DateUtils;
 import android.util.Log;
 
-public class UserHistoryForgettingCurveUtils {
+public final class UserHistoryForgettingCurveUtils {
     private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName();
     private static final boolean DEBUG = false;
     private static final int FC_FREQ_MAX = 127;
@@ -50,7 +50,7 @@
         }
 
         private ForgettingCurveParams(long now, boolean isValid) {
-            this((int)pushCount((byte)0, isValid), now, now, isValid);
+            this(pushCount((byte)0, isValid), now, now, isValid);
         }
 
         /** This constructor is called when the user history bigram dictionary is being restored. */
@@ -199,20 +199,20 @@
         public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
         static {
             for (int i = 0; i < FC_LEVEL_MAX; ++i) {
-                final double initialFreq;
+                final float initialFreq;
                 if (i >= 2) {
-                    initialFreq = (double)FC_FREQ_MAX;
+                    initialFreq = FC_FREQ_MAX;
                 } else if (i == 1) {
-                    initialFreq = (double)FC_FREQ_MAX / 2;
+                    initialFreq = FC_FREQ_MAX / 2;
                 } else if (i == 0) {
-                    initialFreq = (double)FC_FREQ_MAX / 4;
+                    initialFreq = FC_FREQ_MAX / 4;
                 } else {
                     continue;
                 }
                 for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
-                    final double elapsedHour = j * ELAPSED_TIME_INTERVAL_HOURS;
-                    final double freq =
-                            initialFreq * Math.pow(initialFreq, elapsedHour / HALF_LIFE_HOURS);
+                    final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
+                    final float freq = initialFreq
+                            * (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 4178955..876bc8e 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -16,20 +16,16 @@
 
 package com.android.inputmethod.latin;
 
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.content.res.Resources;
 import android.inputmethodservice.InputMethodService;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
 import android.util.Log;
 
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -44,12 +40,9 @@
 import java.io.PrintWriter;
 import java.nio.channels.FileChannel;
 import java.text.SimpleDateFormat;
-import java.util.Collections;
 import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
 
-public class Utils {
+public final class Utils {
     private Utils() {
         // This utility class is not publicly instantiable.
     }
@@ -67,44 +60,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';
@@ -206,19 +161,25 @@
     }
 
     // Get the current stack trace
-    public static String getStackTrace() {
+    public static String getStackTrace(final int limit) {
         StringBuilder sb = new StringBuilder();
         try {
             throw new RuntimeException();
         } catch (RuntimeException e) {
             StackTraceElement[] frames = e.getStackTrace();
             // Start at 1 because the first frame is here and we don't care about it
-            for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n");
+            for (int j = 1; j < frames.length && j < limit + 1; ++j) {
+                sb.append(frames[j].toString() + "\n");
+            }
         }
         return sb.toString();
     }
 
-    public static class UsabilityStudyLogUtils {
+    public static String getStackTrace() {
+        return getStackTrace(Integer.MAX_VALUE - 1);
+    }
+
+    public static final class UsabilityStudyLogUtils {
         // TODO: remove code duplication with ResearchLog class
         private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
         private static final String FILENAME = "log.txt";
@@ -427,34 +388,48 @@
         }
     }
 
-    public static float getDipScale(Context context) {
-        final float scale = context.getResources().getDisplayMetrics().density;
-        return scale;
-    }
-
-    /** Convert pixel to DIP */
-    public static int dipToPixel(float scale, int dip) {
-        return (int) (dip * scale + 0.5);
-    }
-
-    public static class Stats {
+    public static final class Stats {
         public static void onNonSeparator(final char code, final int x,
                 final int y) {
             RingCharBuffer.getInstance().push(code, x, y);
             LatinImeLogger.logOnInputChar();
         }
 
-        public static void onSeparator(final int code, final int x,
-                final int y) {
-            // TODO: accept code points
-            RingCharBuffer.getInstance().push((char)code, x, y);
+        public static void onSeparator(final int code, final int x, final int y) {
+            // Helper method to log a single code point separator
+            // TODO: cache this mapping of a code point to a string in a sparse array in StringUtils
+            onSeparator(new String(new int[]{code}, 0, 1), x, y);
+        }
+
+        public static void onSeparator(final String separator, final int x, final int y) {
+            final int length = separator.length();
+            for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) {
+                int codePoint = Character.codePointAt(separator, i);
+                // TODO: accept code points
+                RingCharBuffer.getInstance().push((char)codePoint, x, y);
+            }
             LatinImeLogger.logOnInputSeparator();
         }
 
         public static void onAutoCorrection(final String typedWord, final String correctedWord,
-                final int separatorCode) {
-            if (TextUtils.isEmpty(typedWord)) return;
-            LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, separatorCode);
+                final String separatorString, final WordComposer wordComposer) {
+            final boolean isBatchMode = wordComposer.isBatchMode();
+            if (!isBatchMode && TextUtils.isEmpty(typedWord)) return;
+            // TODO: this fails when the separator is more than 1 code point long, but
+            // the backend can't handle it yet. The only case when this happens is with
+            // smileys and other multi-character keys.
+            final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE
+                    : separatorString.codePointAt(0);
+            if (!isBatchMode) {
+                LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint);
+            } else {
+                if (!TextUtils.isEmpty(correctedWord)) {
+                    // We must make sure that InputPointer contains only the relative timestamps,
+                    // not actual timestamps.
+                    LatinImeLogger.logOnAutoCorrectionForGeometric(
+                            "", correctedWord, codePoint, wordComposer.getInputPointers());
+                }
+            }
         }
 
         public static void onAutoCorrectionCancellation() {
@@ -470,60 +445,4 @@
         if (TextUtils.isEmpty(info)) return null;
         return info;
     }
-
-    private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
-    private static final HashMap<String, String> sDeviceOverrideValueMap =
-            new HashMap<String, String>();
-
-    public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
-        final int orientation = res.getConfiguration().orientation;
-        final String key = overrideResId + "-" + orientation;
-        if (!sDeviceOverrideValueMap.containsKey(key)) {
-            String overrideValue = defValue;
-            for (final String element : res.getStringArray(overrideResId)) {
-                if (element.startsWith(HARDWARE_PREFIX)) {
-                    overrideValue = element.substring(HARDWARE_PREFIX.length());
-                    break;
-                }
-            }
-            sDeviceOverrideValueMap.put(key, overrideValue);
-        }
-        return sDeviceOverrideValueMap.get(key);
-    }
-
-    private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = new HashMap<String, Long>();
-    private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
-    public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
-        if (TextUtils.isEmpty(str)) {
-            return EMPTY_LT_HASH_MAP;
-        }
-        final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER);
-        final int N = ss.length;
-        if (N < 2 || N % 2 != 0) {
-            return EMPTY_LT_HASH_MAP;
-        }
-        final HashMap<String, Long> retval = new HashMap<String, Long>();
-        for (int i = 0; i < N / 2; ++i) {
-            final String localeStr = ss[i * 2];
-            final long time = Long.valueOf(ss[i * 2 + 1]);
-            retval.put(localeStr, time);
-        }
-        return retval;
-    }
-
-    public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) {
-        if (map == null || map.isEmpty()) {
-            return "";
-        }
-        final StringBuilder builder = new StringBuilder();
-        for (String localeStr : map.keySet()) {
-            if (builder.length() > 0) {
-                builder.append(LOCALE_AND_TIME_STR_SEPARATER);
-            }
-            final Long time = map.get(localeStr);
-            builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER);
-            builder.append(String.valueOf(time));
-        }
-        return builder.toString();
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/VibratorUtils.java b/java/src/com/android/inputmethod/latin/VibratorUtils.java
index 33ffdd9..b6696ce 100644
--- a/java/src/com/android/inputmethod/latin/VibratorUtils.java
+++ b/java/src/com/android/inputmethod/latin/VibratorUtils.java
@@ -19,7 +19,7 @@
 import android.content.Context;
 import android.os.Vibrator;
 
-public class VibratorUtils {
+public final class VibratorUtils {
     private static final VibratorUtils sInstance = new VibratorUtils();
     private Vibrator mVibrator;
 
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 a0de2f9..0000000
--- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
+++ /dev/null
@@ -1,109 +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.latin.LocaleUtils.RunInLocale;
-
-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, Suggest.DIC_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;
-    }
-
-    // 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 ca9caa1..275ebf3 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -17,9 +17,7 @@
 package com.android.inputmethod.latin;
 
 import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardActionListener;
 
 import java.util.Arrays;
 
@@ -27,22 +25,27 @@
  * 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 int[] mXCoordinates;
-    private int[] mYCoordinates;
-    private StringBuilder mTypedWord;
+    private final InputPointers mInputPointers = new InputPointers(N);
+    private final StringBuilder mTypedWord;
     private CharSequence mAutoCorrection;
     private boolean mIsResumed;
+    private boolean mIsBatchMode;
 
     // Cache these values for performance
     private int mCapsCount;
-    private boolean mAutoCapitalized;
+    private int mDigitsCount;
+    private int mCapitalizedMode;
     private int mTrailingSingleQuotesCount;
     private int mCodePointSize;
 
@@ -54,28 +57,24 @@
     public WordComposer() {
         mPrimaryKeyCodes = new int[N];
         mTypedWord = new StringBuilder(N);
-        mXCoordinates = new int[N];
-        mYCoordinates = new int[N];
         mAutoCorrection = null;
         mTrailingSingleQuotesCount = 0;
         mIsResumed = false;
+        mIsBatchMode = false;
         refreshSize();
     }
 
     public WordComposer(WordComposer source) {
-        init(source);
-    }
-
-    public void init(WordComposer source) {
         mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
         mTypedWord = new StringBuilder(source.mTypedWord);
-        mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length);
-        mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length);
+        mInputPointers.copy(source.mInputPointers);
         mCapsCount = source.mCapsCount;
+        mDigitsCount = source.mDigitsCount;
         mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
-        mAutoCapitalized = source.mAutoCapitalized;
+        mCapitalizedMode = source.mCapitalizedMode;
         mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
         mIsResumed = source.mIsResumed;
+        mIsBatchMode = source.mIsBatchMode;
         refreshSize();
     }
 
@@ -86,13 +85,15 @@
         mTypedWord.setLength(0);
         mAutoCorrection = null;
         mCapsCount = 0;
+        mDigitsCount = 0;
         mIsFirstCharCapitalized = false;
         mTrailingSingleQuotesCount = 0;
         mIsResumed = false;
+        mIsBatchMode = false;
         refreshSize();
     }
 
-    public final void refreshSize() {
+    private final void refreshSize() {
         mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
     }
 
@@ -116,12 +117,8 @@
         return mPrimaryKeyCodes[index];
     }
 
-    public int[] getXCoordinates() {
-        return mXCoordinates;
-    }
-
-    public int[] getYCoordinates() {
-        return mYCoordinates;
+    public InputPointers getInputPointers() {
+        return mInputPointers;
     }
 
     private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) {
@@ -129,40 +126,28 @@
         return previous && !Character.isUpperCase(codePoint);
     }
 
-    // TODO: remove input keyDetector
-    public void add(int primaryCode, int x, int y, KeyDetector keyDetector) {
-        final int keyX;
-        final int keyY;
-        if (null == keyDetector
-                || x == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
-                || y == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
-                || x == KeyboardActionListener.NOT_A_TOUCH_COORDINATE
-                || y == KeyboardActionListener.NOT_A_TOUCH_COORDINATE) {
-            keyX = x;
-            keyY = y;
-        } else {
-            keyX = keyDetector.getTouchX(x);
-            keyY = keyDetector.getTouchY(y);
-        }
-        add(primaryCode, keyX, keyY);
-    }
-
     /**
      * Add a new keystroke, with the pressed key's code point with the touch point coordinates.
      */
-    private void add(int primaryCode, int keyX, int keyY) {
+    public void add(int primaryCode, int keyX, int keyY) {
         final int newIndex = size();
         mTypedWord.appendCodePoint(primaryCode);
         refreshSize();
         if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
             mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
                     ? Character.toLowerCase(primaryCode) : primaryCode;
-            mXCoordinates[newIndex] = keyX;
-            mYCoordinates[newIndex] = keyY;
+            // In the batch input mode, the {@code mInputPointers} holds batch input points and
+            // shouldn't be overridden by the "typed key" coordinates
+            // (See {@link #setBatchInputWord}).
+            if (!mIsBatchMode) {
+                // TODO: Set correct pointer id and time
+                mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
+            }
         }
         mIsFirstCharCapitalized = isFirstCharCapitalized(
                 newIndex, primaryCode, mIsFirstCharCapitalized);
         if (Character.isUpperCase(primaryCode)) mCapsCount++;
+        if (Character.isDigit(primaryCode)) mDigitsCount++;
         if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
             ++mTrailingSingleQuotesCount;
         } else {
@@ -171,19 +156,35 @@
         mAutoCorrection = null;
     }
 
+    public void setBatchInputPointers(InputPointers batchPointers) {
+        mInputPointers.set(batchPointers);
+        mIsBatchMode = true;
+    }
+
+    public void setBatchInputWord(CharSequence word) {
+        reset();
+        mIsBatchMode = true;
+        final int length = word.length();
+        for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
+            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, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+        }
+    }
+
     /**
      * Internal method to retrieve reasonable proximity info for a character.
      */
     private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
-        for (final Key key : keyboard.mKeys) {
-            if (key.mCode == codePoint) {
-                final int x = key.mX + key.mWidth / 2;
-                final int y = key.mY + key.mHeight / 2;
-                add(codePoint, x, y);
-                return;
-            }
+        final Key key = keyboard.getKey(codePoint);
+        if (key != null) {
+            final int x = key.mX + key.mWidth / 2;
+            final int y = key.mY + key.mHeight / 2;
+            add(codePoint, x, y);
+            return;
         }
-        add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
+        add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
     }
 
     /**
@@ -219,6 +220,7 @@
                 mTypedWord.deleteCharAt(stringBuilderLength - 1);
             }
             if (Character.isUpperCase(lastChar)) mCapsCount--;
+            if (Character.isDigit(lastChar)) mDigitsCount--;
             refreshSize();
         }
         // We may have deleted the last one.
@@ -263,7 +265,17 @@
      * @return true if all user typed chars are upper case, false otherwise
      */
     public boolean isAllUpperCase() {
-        return (mCapsCount > 0) && (mCapsCount == size());
+        if (size() <= 1) {
+            return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+                    || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
+        } else {
+            return mCapsCount == size();
+        }
+    }
+
+    public boolean wasShiftedNoLock() {
+        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
+                || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
     }
 
     /**
@@ -274,20 +286,34 @@
     }
 
     /**
-     * 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
+     * Returns true if we have digits in the composing word.
      */
-    public void setAutoCapitalized(boolean auto) {
-        mAutoCapitalized = auto;
+    public boolean hasDigits() {
+        return mDigitsCount > 0;
+    }
+
+    /**
+     * 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 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;
     }
 
     /**
@@ -313,24 +339,26 @@
 
     // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
     public LastComposedWord commitWord(final int type, final String committedWord,
-            final int separatorCode, final CharSequence prevWord) {
+            final String separatorString, final CharSequence prevWord) {
         // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
         // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
         // the last composed word to ensure this does not happen.
         final int[] primaryKeyCodes = mPrimaryKeyCodes;
-        final int[] xCoordinates = mXCoordinates;
-        final int[] yCoordinates = mYCoordinates;
         mPrimaryKeyCodes = new int[N];
-        mXCoordinates = new int[N];
-        mYCoordinates = new int[N];
         final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
-                xCoordinates, yCoordinates, mTypedWord.toString(), committedWord, separatorCode,
+                mInputPointers, mTypedWord.toString(), committedWord, separatorString,
                 prevWord);
+        mInputPointers.reset();
         if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
                 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
             lastComposedWord.deactivate();
         }
+        mCapsCount = 0;
+        mDigitsCount = 0;
+        mIsBatchMode = false;
         mTypedWord.setLength(0);
+        mTrailingSingleQuotesCount = 0;
+        mIsFirstCharCapitalized = false;
         refreshSize();
         mAutoCorrection = null;
         mIsResumed = false;
@@ -339,12 +367,15 @@
 
     public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
         mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
-        mXCoordinates = lastComposedWord.mXCoordinates;
-        mYCoordinates = lastComposedWord.mYCoordinates;
+        mInputPointers.set(lastComposedWord.mInputPointers);
         mTypedWord.setLength(0);
         mTypedWord.append(lastComposedWord.mTypedWord);
         refreshSize();
         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
         mIsResumed = true;
     }
+
+    public boolean isBatchMode() {
+        return mIsBatchMode;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/XmlParseUtils.java
index 481cdfa..b5cbaf1 100644
--- a/java/src/com/android/inputmethod/latin/XmlParseUtils.java
+++ b/java/src/com/android/inputmethod/latin/XmlParseUtils.java
@@ -23,7 +23,7 @@
 
 import java.io.IOException;
 
-public class XmlParseUtils {
+public final class XmlParseUtils {
     private XmlParseUtils() {
         // This utility class is not publicly instantiable.
     }
diff --git a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
index de20576..52c066a 100644
--- a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
+++ b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
@@ -22,4 +22,5 @@
     }
 
     public static final boolean IS_EXPERIMENTAL = false;
+    public static final boolean IS_INTERNAL = false;
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
new file mode 100644
index 0000000..3975329
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -0,0 +1,226 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Stack;
+
+public class BinaryDictIOUtils {
+    private static final boolean DBG = false;
+
+    private static class Position {
+        public static final int NOT_READ_GROUPCOUNT = -1;
+
+        public int mAddress;
+        public int mNumOfCharGroup;
+        public int mPosition;
+        public int mLength;
+
+        public Position(int address, int length) {
+            mAddress = address;
+            mLength = length;
+            mNumOfCharGroup = NOT_READ_GROUPCOUNT;
+        }
+    }
+
+    /**
+     * Tours all node without recursive call.
+     */
+    private static void readUnigramsAndBigramsBinaryInner(
+            final FusionDictionaryBufferInterface buffer, final int headerSize,
+            final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
+            final Map<Integer, ArrayList<PendingAttribute>> bigrams,
+            final FormatOptions formatOptions) {
+        int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1];
+
+        Stack<Position> stack = new Stack<Position>();
+        int index = 0;
+
+        Position initPos = new Position(headerSize, 0);
+        stack.push(initPos);
+
+        while (!stack.empty()) {
+            Position p = stack.peek();
+
+            if (DBG) {
+                MakedictLog.d("read: address=" + p.mAddress + ", numOfCharGroup=" +
+                        p.mNumOfCharGroup + ", position=" + p.mPosition + ", length=" + p.mLength);
+            }
+
+            if (buffer.position() != p.mAddress) buffer.position(p.mAddress);
+            if (index != p.mLength) index = p.mLength;
+
+            if (p.mNumOfCharGroup == Position.NOT_READ_GROUPCOUNT) {
+                p.mNumOfCharGroup = BinaryDictInputOutput.readCharGroupCount(buffer);
+                p.mAddress += BinaryDictInputOutput.getGroupCountSize(p.mNumOfCharGroup);
+                p.mPosition = 0;
+            }
+
+            CharGroupInfo info = BinaryDictInputOutput.readCharGroup(buffer,
+                    p.mAddress - headerSize, formatOptions);
+            for (int i = 0; i < info.mCharacters.length; ++i) {
+                pushedChars[index++] = info.mCharacters[i];
+            }
+            p.mPosition++;
+
+            if (info.mFrequency != FusionDictionary.CharGroup.NOT_A_TERMINAL) { // found word
+                words.put(info.mOriginalAddress, new String(pushedChars, 0, index));
+                frequencies.put(info.mOriginalAddress, info.mFrequency);
+                if (info.mBigrams != null) bigrams.put(info.mOriginalAddress, info.mBigrams);
+            }
+
+            if (p.mPosition == p.mNumOfCharGroup) {
+                if (formatOptions.mHasLinkedListNode) {
+                    final int forwardLinkAddress = buffer.readUnsignedInt24();
+                    if (forwardLinkAddress != FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+                        // the node has a forward link.
+                        p.mNumOfCharGroup = Position.NOT_READ_GROUPCOUNT;
+                        p.mAddress = forwardLinkAddress;
+                    } else {
+                        stack.pop();
+                    }
+                } else {
+                    stack.pop();
+                }
+            } else {
+                // the node has more groups.
+                p.mAddress = buffer.position();
+            }
+
+            if (BinaryDictInputOutput.hasChildrenAddress(info.mChildrenAddress)) {
+                Position childrenPos = new Position(info.mChildrenAddress + headerSize, index);
+                stack.push(childrenPos);
+            }
+        }
+    }
+
+    /**
+     * Reads unigrams and bigrams from the binary file.
+     * Doesn't make the memory representation of the dictionary.
+     *
+     * @param buffer the buffer to read.
+     * @param words the map to store the address as a key and the word as a value.
+     * @param frequencies the map to store the address as a key and the frequency as a value.
+     * @param bigrams the map to store the address as a key and the list of address as a value.
+     * @throws IOException
+     * @throws UnsupportedFormatException
+     */
+    public static void readUnigramsAndBigramsBinary(final FusionDictionaryBufferInterface buffer,
+            final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
+            final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException,
+            UnsupportedFormatException {
+        // Read header
+        final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+        readUnigramsAndBigramsBinaryInner(buffer, header.mHeaderSize, words, frequencies, bigrams,
+                header.mFormatOptions);
+    }
+
+    /**
+     * Gets the address of the last CharGroup of the exact matching word in the dictionary.
+     * If no match is found, returns NOT_VALID_WORD.
+     *
+     * @param buffer the buffer to read.
+     * @param word the word we search for.
+     * @return the address of the terminal node.
+     * @throws IOException
+     * @throws UnsupportedFormatException
+     */
+    public static int getTerminalPosition(final FusionDictionaryBufferInterface buffer,
+            final String word) throws IOException, UnsupportedFormatException {
+        if (word == null) return FormatSpec.NOT_VALID_WORD;
+        if (buffer.position() != 0) buffer.position(0);
+
+        final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+        int wordPos = 0;
+        final int wordLen = word.codePointCount(0, word.length());
+        for (int depth = 0; depth < Constants.Dictionary.MAX_WORD_LENGTH; ++depth) {
+            if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD;
+            int groupOffset = buffer.position() - header.mHeaderSize;
+            final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
+            groupOffset += BinaryDictInputOutput.getGroupCountSize(charGroupCount);
+
+            for (int i = 0; i < charGroupCount; ++i) {
+                final int charGroupPos = buffer.position();
+                final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
+                        buffer.position(), header.mFormatOptions);
+                boolean same = true;
+                for (int p = 0, j = word.offsetByCodePoints(0, wordPos);
+                        p < currentInfo.mCharacters.length;
+                        ++p, j = word.offsetByCodePoints(j, 1)) {
+                    if (wordPos + p >= wordLen
+                            || word.codePointAt(j) != currentInfo.mCharacters[p]) {
+                        same = false;
+                        break;
+                    }
+                }
+
+                if (same) {
+                    if (wordPos + currentInfo.mCharacters.length == wordLen) {
+                        if (currentInfo.mFrequency == CharGroup.NOT_A_TERMINAL) {
+                            return FormatSpec.NOT_VALID_WORD;
+                        } else {
+                            return charGroupPos;
+                        }
+                    }
+                    wordPos += currentInfo.mCharacters.length;
+                    if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
+                        return FormatSpec.NOT_VALID_WORD;
+                    }
+                    buffer.position(currentInfo.mChildrenAddress);
+                    break;
+                }
+                groupOffset = currentInfo.mEndAddress;
+
+                // not found
+                if (i >= charGroupCount - 1) {
+                    return FormatSpec.NOT_VALID_WORD;
+                }
+            }
+        }
+        return FormatSpec.NOT_VALID_WORD;
+    }
+
+    /**
+     * Delete the word from the binary file.
+     *
+     * @param buffer the buffer to write.
+     * @param word the word we delete
+     * @throws IOException
+     * @throws UnsupportedFormatException
+     */
+    public static void deleteWord(final FusionDictionaryBufferInterface buffer,
+            final String word) throws IOException, UnsupportedFormatException {
+        buffer.position(0);
+        final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+        final int wordPosition = getTerminalPosition(buffer, word);
+        if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
+
+        buffer.position(wordPosition);
+        final int flags = buffer.readUnsignedByte();
+        final int newFlags = flags ^ FormatSpec.FLAG_IS_TERMINAL;
+        buffer.position(wordPosition);
+        buffer.put((byte)newFlags);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
index 89c59f8..7b8dc5c 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -16,21 +16,27 @@
 
 package com.android.inputmethod.latin.makedict;
 
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
 import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
 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;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Stack;
 import java.util.TreeMap;
 
 /**
@@ -40,143 +46,7 @@
  */
 public class BinaryDictInputOutput {
 
-    final static boolean DBG = MakedictLog.DBG;
-
-    /* Node layout is as follows:
-     *   | addressType                         xx     : mask with MASK_GROUP_ADDRESS_TYPE
-     *                                 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
-     * f |                                     01 = 1 byte      : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
-     * l |                                     10 = 2 bytes     : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
-     * a |                                     11 = 3 bytes     : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
-     * g | has several chars ?         1 bit, 1 = yes, 0 = no   : FLAG_HAS_MULTIPLE_CHARS
-     * s | has a terminal ?            1 bit, 1 = yes, 0 = no   : FLAG_IS_TERMINAL
-     *   | has shortcut targets ?      1 bit, 1 = yes, 0 = no   : FLAG_HAS_SHORTCUT_TARGETS
-     *   | has bigrams ?               1 bit, 1 = yes, 0 = no   : FLAG_HAS_BIGRAMS
-     *
-     * c | IF FLAG_HAS_MULTIPLE_CHARS
-     * h |   char, char, char, char    n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
-     * a |   end                       1 byte, = 0
-     * r | ELSE
-     * s |   char                      1 or 3 bytes
-     *   | END
-     *
-     * f |
-     * r | IF FLAG_IS_TERMINAL
-     * e |   frequency                 1 byte
-     * q |
-     *
-     * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
-     * h |   // nothing
-     * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
-     * l |   children address, 1 byte
-     * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
-     * r |   children address, 2 bytes
-     * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
-     * n |   children address, 3 bytes
-     * A | END
-     * d
-     * dress
-     *
-     *   | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
-     *   | shortcut string list
-     *   | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
-     *   | bigrams address list
-     *
-     * Char format is:
-     * 1 byte = bbbbbbbb match
-     * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
-     * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
-     *       unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
-     *       00011111 would be outside unicode.
-     * else: iso-latin-1 code
-     * This allows for the whole unicode range to be encoded, including chars outside of
-     * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
-     * characters which should never happen anyway (and still work, but take 3 bytes).
-     *
-     * bigram address list is:
-     * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no     : FLAG_ATTRIBUTE_HAS_NEXT
-     *           | addressSign = 1 bit,                 : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
-     *           |                      1 = must take -address, 0 = must take +address
-     *           |                         xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
-     *           | addressFormat = 2 bits, 00 = unused  : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
-     *           |                         01 = 1 byte  : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
-     *           |                         10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
-     *           |                         11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
-     *           | 4 bits : frequency         : mask with FLAG_ATTRIBUTE_FREQUENCY
-     * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
-     *           |   read 1 byte, add top 4 bits
-     *           | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
-     *           |   read 2 bytes, add top 4 bits
-     *           | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
-     *           |   read 3 bytes, add top 4 bits
-     *           | END
-     *           | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
-     * if (FLAG_ATTRIBUTE_HAS_NEXT) goto bigram_and_shortcut_address_list_is
-     *
-     * shortcut string list is:
-     * <byte size> = GROUP_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes.
-     * <flags>     = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
-     *               | reserved = 3 bits, must be 0
-     *               | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
-     * <shortcut>  = | string of characters at the char format described above, with the terminator
-     *               | used to signal the end of the string.
-     * if (FLAG_ATTRIBUTE_HAS_NEXT goto flags
-     */
-
-    private static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
-    private 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;
-    private static final int FIRST_VERSION_WITH_HEADER_SIZE = 2;
-
-    // These options need to be the same numeric values as the one in the native reading code.
-    private static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
-    private static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4;
-    private static final int CONTAINS_BIGRAMS_FLAG = 0x8;
-
-    // TODO: Make this value adaptative to content data, store it in the header, and
-    // use it in the reading code.
-    private static final int MAX_WORD_LENGTH = 48;
-
-    private static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
-    private static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
-    private static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
-    private static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
-    private static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
-
-    private static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
-
-    private static final int FLAG_IS_TERMINAL = 0x10;
-    private static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
-    private static final int FLAG_HAS_BIGRAMS = 0x04;
-
-    private static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
-    private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
-    private static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
-    private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
-    private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
-    private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
-    private static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
-
-    private static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
-
-    private static final int GROUP_TERMINATOR_SIZE = 1;
-    private static final int GROUP_FLAGS_SIZE = 1;
-    private static final int GROUP_FREQUENCY_SIZE = 1;
-    private static final int GROUP_MAX_ADDRESS_SIZE = 3;
-    private static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
-    private static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
-    private static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2;
-
-    private static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
-    private static final int INVALID_CHARACTER = -1;
-
-    private static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127
-    private static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767
-
-    private static final int MAX_TERMINAL_FREQUENCY = 255;
-    private static final int MAX_BIGRAM_FREQUENCY = 15;
+    private static final boolean DBG = MakedictLog.DBG;
 
     // Arbitrary limit to how much passes we consider address size compression should
     // terminate in. At the time of this writing, our largest dictionary completes
@@ -185,6 +55,66 @@
     // suspicion that a bug might be causing an infinite loop.
     private static final int MAX_PASSES = 24;
 
+    public interface FusionDictionaryBufferInterface {
+        public int readUnsignedByte();
+        public int readUnsignedShort();
+        public int readUnsignedInt24();
+        public int readInt();
+        public int position();
+        public void position(int newPosition);
+        public void put(final byte b);
+        public int limit();
+    }
+
+    public static final class ByteBufferWrapper implements FusionDictionaryBufferInterface {
+        private ByteBuffer mBuffer;
+
+        public ByteBufferWrapper(final ByteBuffer buffer) {
+            mBuffer = buffer;
+        }
+
+        @Override
+        public int readUnsignedByte() {
+            return ((int)mBuffer.get()) & 0xFF;
+        }
+
+        @Override
+        public int readUnsignedShort() {
+            return ((int)mBuffer.getShort()) & 0xFFFF;
+        }
+
+        @Override
+        public int readUnsignedInt24() {
+            final int retval = readUnsignedByte();
+            return (retval << 16) + readUnsignedShort();
+        }
+
+        @Override
+        public int readInt() {
+            return mBuffer.getInt();
+        }
+
+        @Override
+        public int position() {
+            return mBuffer.position();
+        }
+
+        @Override
+        public void position(int newPos) {
+            mBuffer.position(newPos);
+        }
+
+        @Override
+        public void put(final byte b) {
+            mBuffer.put(b);
+        }
+
+        @Override
+        public int limit() {
+            return mBuffer.limit();
+        }
+    }
+
     /**
      * A class grouping utility function for our specific character encoding.
      */
@@ -196,7 +126,7 @@
         /**
          * Helper method to find out whether this code fits on one byte
          */
-        private static boolean fitsOnOneByte(int character) {
+        private static boolean fitsOnOneByte(final int character) {
             return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
                     && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
         }
@@ -218,10 +148,10 @@
          * @param character the character code.
          * @return the size in binary encoded-form, either 1 or 3 bytes.
          */
-        private static int getCharSize(int character) {
+        private static int getCharSize(final int character) {
             // See char encoding in FusionDictionary.java
             if (fitsOnOneByte(character)) return 1;
-            if (INVALID_CHARACTER == character) return 1;
+            if (FormatSpec.INVALID_CHARACTER == character) return 1;
             return 3;
         }
 
@@ -279,7 +209,7 @@
                     buffer[index++] = (byte)(0xFF & codePoint);
                 }
             }
-            buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+            buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR;
             return index - origin;
         }
 
@@ -291,7 +221,7 @@
          * @param buffer the ByteArrayOutputStream to write to.
          * @param word the string to write.
          */
-        private static void writeString(ByteArrayOutputStream buffer, final String word) {
+        private static void writeString(final ByteArrayOutputStream buffer, 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);
@@ -303,37 +233,38 @@
                     buffer.write((byte) (0xFF & codePoint));
                 }
             }
-            buffer.write(GROUP_CHARACTERS_TERMINATOR);
+            buffer.write(FormatSpec.GROUP_CHARACTERS_TERMINATOR);
         }
 
         /**
-         * Reads a string from a RandomAccessFile. This is the converse of the above method.
+         * Reads a string from a buffer. This is the converse of the above method.
          */
-        private static String readString(final RandomAccessFile source) throws IOException {
+        private static String readString(final FusionDictionaryBufferInterface buffer) {
             final StringBuilder s = new StringBuilder();
-            int character = readChar(source);
-            while (character != INVALID_CHARACTER) {
+            int character = readChar(buffer);
+            while (character != FormatSpec.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 buffer.
          *
          * 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 FusionDictionaryBufferInterface buffer) {
+            int character = buffer.readUnsignedByte();
             if (!fitsOnOneByte(character)) {
-                if (GROUP_CHARACTERS_TERMINATOR == character)
-                    return INVALID_CHARACTER;
+                if (FormatSpec.GROUP_CHARACTERS_TERMINATOR == character) {
+                    return FormatSpec.INVALID_CHARACTER;
+                }
                 character <<= 16;
-                character += source.readUnsignedShort();
+                character += buffer.readUnsignedShort();
             }
             return character;
         }
@@ -348,9 +279,9 @@
      * @param group the group
      * @return the size of the char array, including the terminator if any
      */
-    private static int getGroupCharactersSize(CharGroup group) {
+    private static int getGroupCharactersSize(final CharGroup group) {
         int size = CharEncoding.getCharArraySize(group.mChars);
-        if (group.hasSeveralChars()) size += GROUP_TERMINATOR_SIZE;
+        if (group.hasSeveralChars()) size += FormatSpec.GROUP_TERMINATOR_SIZE;
         return size;
     }
 
@@ -359,14 +290,15 @@
      * @param count the group count
      * @return the size of the group count, either 1 or 2 bytes.
      */
-    private static int getGroupCountSize(final int count) {
-        if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
+    public static int getGroupCountSize(final int count) {
+        if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
             return 1;
-        } else if (MAX_CHARGROUPS_IN_A_NODE >= count) {
+        } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) {
             return 2;
         } else {
-            throw new RuntimeException("Can't have more than " + MAX_CHARGROUPS_IN_A_NODE
-                    + " groups in a node (found " + count +")");
+            throw new RuntimeException("Can't have more than "
+                    + FormatSpec.MAX_CHARGROUPS_IN_A_NODE + " groups in a node (found " + count
+                    + ")");
         }
     }
 
@@ -383,14 +315,14 @@
      * Compute the size of a shortcut in bytes.
      */
     private static int getShortcutSize(final WeightedString shortcut) {
-        int size = GROUP_ATTRIBUTE_FLAGS_SIZE;
+        int size = FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE;
         final String word = shortcut.mWord;
         final int length = word.length();
         for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
             final int codePoint = word.codePointAt(i);
             size += CharEncoding.getCharSize(codePoint);
         }
-        size += GROUP_TERMINATOR_SIZE;
+        size += FormatSpec.GROUP_TERMINATOR_SIZE;
         return size;
     }
 
@@ -402,7 +334,7 @@
      */
     private static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) {
         if (null == shortcutList) return 0;
-        int size = GROUP_SHORTCUT_LIST_SIZE_SIZE;
+        int size = FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
         for (final WeightedString shortcut : shortcutList) {
             size += getShortcutSize(shortcut);
         }
@@ -413,16 +345,18 @@
      * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything.
      *
      * @param group the CharGroup to compute the size of.
+     * @param options file format options.
      * @return the maximum size of the group.
      */
-    private static int getCharGroupMaximumSize(CharGroup group) {
-        int size = getGroupCharactersSize(group) + GROUP_FLAGS_SIZE;
+    private static int getCharGroupMaximumSize(final CharGroup group, final FormatOptions options) {
+        int size = getGroupHeaderSize(group, options);
         // If terminal, one byte for the frequency
-        if (group.isTerminal()) size += GROUP_FREQUENCY_SIZE;
-        size += GROUP_MAX_ADDRESS_SIZE; // For children address
+        if (group.isTerminal()) size += FormatSpec.GROUP_FREQUENCY_SIZE;
+        size += FormatSpec.GROUP_MAX_ADDRESS_SIZE; // For children address
         size += getShortcutListSize(group.mShortcutTargets);
         if (null != group.mBigrams) {
-            size += (GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
+            size += (FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE
+                    + FormatSpec.GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
                     * group.mBigrams.size();
         }
         return size;
@@ -433,22 +367,49 @@
      * it in the 'actualSize' member of the node.
      *
      * @param node the node to compute the maximum size of.
+     * @param options file format options.
      */
-    private static void setNodeMaximumSize(Node node) {
+    private static void setNodeMaximumSize(final Node node, final FormatOptions options) {
         int size = getGroupCountSize(node);
         for (CharGroup g : node.mData) {
-            final int groupSize = getCharGroupMaximumSize(g);
+            final int groupSize = getCharGroupMaximumSize(g, options);
             g.mCachedSize = groupSize;
             size += groupSize;
         }
+        if (options.mHasLinkedListNode) {
+            size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+        }
         node.mCachedSize = size;
     }
 
     /**
      * Helper method to hide the actual value of the no children address.
      */
-    private static boolean hasChildrenAddress(int address) {
-        return NO_CHILDREN_ADDRESS != address;
+    public static boolean hasChildrenAddress(final int address) {
+        return FormatSpec.NO_CHILDREN_ADDRESS != address;
+    }
+
+    /**
+     * Helper method to check whether the CharGroup has a parent address.
+     */
+    public static boolean hasParentAddress(final FormatOptions options) {
+        return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_PARENT_ADDRESS
+                && options.mHasParentAddress;
+    }
+
+    /**
+     * Compute the size of the header (flag + [parent address] + characters size) of a CharGroup.
+     *
+     * @param group the group of which to compute the size of the header
+     * @param options file format options.
+     */
+    private static int getGroupHeaderSize(final CharGroup group, final FormatOptions options) {
+        if (hasParentAddress(options)) {
+            return FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
+                    + getGroupCharactersSize(group);
+        } else {
+            return FormatSpec.GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+        }
     }
 
     /**
@@ -461,7 +422,7 @@
      * @param address the address
      * @return the byte size.
      */
-    private static int getByteSize(int address) {
+    private static int getByteSize(final int address) {
         assert(address < 0x1000000);
         if (!hasChildrenAddress(address)) {
             return 0;
@@ -477,14 +438,14 @@
 
     // This method is responsible for finding a nice ordering of the nodes that favors run-time
     // cache performance and dictionary size.
-    /* package for tests */ static ArrayList<Node> flattenTree(Node root) {
+    /* package for tests */ static ArrayList<Node> flattenTree(final Node root) {
         final int treeSize = FusionDictionary.countCharGroups(root);
         MakedictLog.i("Counted nodes : " + treeSize);
         final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize);
         return flattenTreeInner(flatTree, root);
     }
 
-    private static ArrayList<Node> flattenTreeInner(ArrayList<Node> list, Node node) {
+    private static ArrayList<Node> flattenTreeInner(final ArrayList<Node> list, final Node node) {
         // Removing the node is necessary if the tails are merged, because we would then
         // add the same node several times when we only want it once. A number of places in
         // the code also depends on any node being only once in the list.
@@ -534,9 +495,11 @@
      *
      * @param node the node to compute the size of.
      * @param dict the dictionary in which the word/attributes are to be found.
+     * @param formatOptions file format options.
      * @return false if none of the cached addresses inside the node changed, true otherwise.
      */
-    private static boolean computeActualNodeSize(Node node, FusionDictionary dict) {
+    private static boolean computeActualNodeSize(final Node node, final FusionDictionary dict,
+            final FormatOptions formatOptions) {
         boolean changed = false;
         int size = getGroupCountSize(node);
         for (CharGroup group : node.mData) {
@@ -544,26 +507,32 @@
                 changed = true;
                 group.mCachedAddress = node.mCachedAddress + size;
             }
-            int groupSize = GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
-            if (group.isTerminal()) groupSize += GROUP_FREQUENCY_SIZE;
+            int groupSize = getGroupHeaderSize(group, formatOptions);
+            if (group.isTerminal()) groupSize += FormatSpec.GROUP_FREQUENCY_SIZE;
             if (null != group.mChildren) {
-                final int offsetBasePoint= groupSize + node.mCachedAddress + size;
+                final int offsetBasePoint = groupSize + node.mCachedAddress + size;
                 final int offset = group.mChildren.mCachedAddress - offsetBasePoint;
+                // assign my address to children's parent address
+                group.mChildren.mCachedParentAddress = group.mCachedAddress
+                        - group.mChildren.mCachedAddress;
                 groupSize += getByteSize(offset);
             }
             groupSize += getShortcutListSize(group.mShortcutTargets);
             if (null != group.mBigrams) {
                 for (WeightedString bigram : group.mBigrams) {
                     final int offsetBasePoint = groupSize + node.mCachedAddress + size
-                            + GROUP_FLAGS_SIZE;
+                            + FormatSpec.GROUP_FLAGS_SIZE;
                     final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
                     final int offset = addressOfBigram - offsetBasePoint;
-                    groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE;
+                    groupSize += getByteSize(offset) + FormatSpec.GROUP_FLAGS_SIZE;
                 }
             }
             group.mCachedSize = groupSize;
             size += groupSize;
         }
+        if (formatOptions.mHasLinkedListNode) {
+            size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+        }
         if (node.mCachedSize != size) {
             node.mCachedSize = size;
             changed = true;
@@ -575,9 +544,11 @@
      * Computes the byte size of a list of nodes and updates each node cached position.
      *
      * @param flatNodes the array of nodes.
+     * @param formatOptions file format options.
      * @return the byte size of the entire stack.
      */
-    private static int stackNodes(ArrayList<Node> flatNodes) {
+    private static int stackNodes(final ArrayList<Node> flatNodes,
+            final FormatOptions formatOptions) {
         int nodeOffset = 0;
         for (Node n : flatNodes) {
             n.mCachedAddress = nodeOffset;
@@ -587,7 +558,9 @@
                 g.mCachedAddress = groupCountSize + nodeOffset + groupOffset;
                 groupOffset += g.mCachedSize;
             }
-            if (groupOffset + groupCountSize != n.mCachedSize) {
+            final int nodeSize = groupCountSize + groupOffset
+                    + (formatOptions.mHasLinkedListNode ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0);
+            if (nodeSize != n.mCachedSize) {
                 throw new RuntimeException("Bug : Stored and computed node size differ");
             }
             nodeOffset += n.mCachedSize;
@@ -607,13 +580,14 @@
      *
      * @param dict the dictionary
      * @param flatNodes the ordered array of nodes
+     * @param formatOptions file format options.
      * @return the same array it was passed. The nodes have been updated for address and size.
      */
-    private static ArrayList<Node> computeAddresses(FusionDictionary dict,
-            ArrayList<Node> flatNodes) {
+    private static ArrayList<Node> computeAddresses(final FusionDictionary dict,
+            final ArrayList<Node> flatNodes, final FormatOptions formatOptions) {
         // First get the worst sizes and offsets
-        for (Node n : flatNodes) setNodeMaximumSize(n);
-        final int offset = stackNodes(flatNodes);
+        for (Node n : flatNodes) setNodeMaximumSize(n, formatOptions);
+        final int offset = stackNodes(flatNodes, formatOptions);
 
         MakedictLog.i("Compressing the array addresses. Original size : " + offset);
         MakedictLog.i("(Recursively seen size : " + offset + ")");
@@ -624,12 +598,12 @@
             changesDone = false;
             for (Node n : flatNodes) {
                 final int oldNodeSize = n.mCachedSize;
-                final boolean changed = computeActualNodeSize(n, dict);
+                final boolean changed = computeActualNodeSize(n, dict, formatOptions);
                 final int newNodeSize = n.mCachedSize;
                 if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
                 changesDone |= changed;
             }
-            stackNodes(flatNodes);
+            stackNodes(flatNodes, formatOptions);
             ++passes;
             if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug");
         } while (changesDone);
@@ -652,7 +626,7 @@
      *
      * @param array the array node to check
      */
-    private static void checkFlatNodeArray(ArrayList<Node> array) {
+    private static void checkFlatNodeArray(final ArrayList<Node> array) {
         int offset = 0;
         int index = 0;
         for (Node n : array) {
@@ -697,20 +671,20 @@
     private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
             final int childrenOffset) {
         byte flags = 0;
-        if (group.mChars.length > 1) flags |= FLAG_HAS_MULTIPLE_CHARS;
+        if (group.mChars.length > 1) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS;
         if (group.mFrequency >= 0) {
-            flags |= FLAG_IS_TERMINAL;
+            flags |= FormatSpec.FLAG_IS_TERMINAL;
         }
         if (null != group.mChildren) {
             switch (getByteSize(childrenOffset)) {
              case 1:
-                 flags |= FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
+                 flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
                  break;
              case 2:
-                 flags |= FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
+                 flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
                  break;
              case 3:
-                 flags |= FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
+                 flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
                  break;
              default:
                  throw new RuntimeException("Node with a strange address");
@@ -720,13 +694,19 @@
             if (DBG && 0 == group.mShortcutTargets.size()) {
                 throw new RuntimeException("0-sized shortcut list must be null");
             }
-            flags |= FLAG_HAS_SHORTCUT_TARGETS;
+            flags |= FormatSpec.FLAG_HAS_SHORTCUT_TARGETS;
         }
         if (null != group.mBigrams) {
             if (DBG && 0 == group.mBigrams.size()) {
                 throw new RuntimeException("0-sized bigram list must be null");
             }
-            flags |= FLAG_HAS_BIGRAMS;
+            flags |= FormatSpec.FLAG_HAS_BIGRAMS;
+        }
+        if (group.mIsNotAWord) {
+            flags |= FormatSpec.FLAG_IS_NOT_A_WORD;
+        }
+        if (group.mIsBlacklistEntry) {
+            flags |= FormatSpec.FLAG_IS_BLACKLISTED;
         }
         return flags;
     }
@@ -743,17 +723,17 @@
      */
     private static final int makeBigramFlags(final boolean more, final int offset,
             int bigramFrequency, final int unigramFrequency, final String word) {
-        int bigramFlags = (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0)
-                + (offset < 0 ? FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
+        int bigramFlags = (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0)
+                + (offset < 0 ? FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
         switch (getByteSize(offset)) {
         case 1:
-            bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+            bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
             break;
         case 2:
-            bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+            bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
             break;
         case 3:
-            bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+            bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
             break;
         default:
             throw new RuntimeException("Strange offset size");
@@ -783,13 +763,14 @@
         // 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.
-        final double stepSize =
-                (double)(MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5 + MAX_BIGRAM_FREQUENCY);
-        final double firstStepStart = 1 + unigramFrequency + (stepSize / 2.0);
+        // 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 =
+                (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency)
+                / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY);
+        final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f);
         final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize);
         // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1
         // here. The best approximation would be the unigram freq itself, so we should not
@@ -797,19 +778,22 @@
         // small over-estimation that we get in this case. TODO: actually remove this bigram
         // if discretizedFrequency < 0.
         final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0;
-        bigramFlags += finalBigramFrequency & FLAG_ATTRIBUTE_FREQUENCY;
+        bigramFlags += finalBigramFrequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY;
         return bigramFlags;
     }
 
     /**
      * Makes the 2-byte value for options flags.
      */
-    private static final int makeOptionsValue(final FusionDictionary dictionary) {
+    private static final int makeOptionsValue(final FusionDictionary dictionary,
+            final FormatOptions formatOptions) {
         final DictionaryOptions options = dictionary.mOptions;
         final boolean hasBigrams = dictionary.hasBigrams();
-        return (options.mFrenchLigatureProcessing ? FRENCH_LIGATURE_PROCESSING_FLAG : 0)
-                + (options.mGermanUmlautProcessing ? GERMAN_UMLAUT_PROCESSING_FLAG : 0)
-                + (hasBigrams ? CONTAINS_BIGRAMS_FLAG : 0);
+        return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0)
+                + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0)
+                + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0)
+                + (formatOptions.mHasParentAddress ? FormatSpec.HAS_PARENT_ADDRESS : 0)
+                + (formatOptions.mHasLinkedListNode ? FormatSpec.HAS_LINKEDLIST_NODE : 0);
     }
 
     /**
@@ -820,7 +804,8 @@
      * @return the flags
      */
     private static final int makeShortcutFlags(final boolean more, final int frequency) {
-        return (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0) + (frequency & FLAG_ATTRIBUTE_FREQUENCY);
+        return (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0)
+                + (frequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY);
     }
 
     /**
@@ -832,13 +817,16 @@
      * @param dict the dictionary the node is a part of (for relative offsets).
      * @param buffer the memory buffer to write to.
      * @param node the node to write.
+     * @param formatOptions file format options.
      * @return the address of the END of the node.
      */
-    private static int writePlacedNode(FusionDictionary dict, byte[] buffer, Node node) {
+    private static int writePlacedNode(final FusionDictionary dict, byte[] buffer,
+            final Node node, final FormatOptions formatOptions) {
         int index = node.mCachedAddress;
 
         final int groupCount = node.mData.size();
         final int countSize = getGroupCountSize(node);
+        final int parentAddress = node.mCachedParentAddress;
         if (1 == countSize) {
             buffer[index++] = (byte)groupCount;
         } else if (2 == countSize) {
@@ -855,20 +843,38 @@
             if (index != group.mCachedAddress) throw new RuntimeException("Bug: write index is not "
                     + "the same as the cached address of the group : "
                     + index + " <> " + group.mCachedAddress);
-            groupAddress += GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+            groupAddress += getGroupHeaderSize(group, formatOptions);
             // Sanity checks.
-            if (DBG && group.mFrequency > MAX_TERMINAL_FREQUENCY) {
-                throw new RuntimeException("A node has a frequency > " + MAX_TERMINAL_FREQUENCY
+            if (DBG && group.mFrequency > FormatSpec.MAX_TERMINAL_FREQUENCY) {
+                throw new RuntimeException("A node has a frequency > "
+                        + FormatSpec.MAX_TERMINAL_FREQUENCY
                         + " : " + group.mFrequency);
             }
-            if (group.mFrequency >= 0) groupAddress += GROUP_FREQUENCY_SIZE;
+            if (group.mFrequency >= 0) groupAddress += FormatSpec.GROUP_FREQUENCY_SIZE;
             final int childrenOffset = null == group.mChildren
-                    ? NO_CHILDREN_ADDRESS : group.mChildren.mCachedAddress - groupAddress;
+                    ? FormatSpec.NO_CHILDREN_ADDRESS
+                            : group.mChildren.mCachedAddress - groupAddress;
             byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset);
             buffer[index++] = flags;
+
+            if (hasParentAddress(formatOptions)) {
+                if (parentAddress == FormatSpec.NO_PARENT_ADDRESS) {
+                    // this node is the root node.
+                    buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
+                } else {
+                    // write parent address. (version 3)
+                    final int actualParentAddress = Math.abs(parentAddress
+                            + (node.mCachedAddress - group.mCachedAddress));
+                    buffer[index] = (byte)((actualParentAddress >> 16) & 0xFF);
+                    buffer[index + 1] = (byte)((actualParentAddress >> 8) & 0xFF);
+                    buffer[index + 2] = (byte)(actualParentAddress & 0xFF);
+                }
+                index += 3;
+            }
+
             index = CharEncoding.writeCharArray(group.mChars, buffer, index);
             if (group.hasSeveralChars()) {
-                buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+                buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR;
             }
             if (group.mFrequency >= 0) {
                 buffer[index++] = (byte) group.mFrequency;
@@ -880,8 +886,8 @@
             // Write shortcuts
             if (null != group.mShortcutTargets) {
                 final int indexOfShortcutByteSize = index;
-                index += GROUP_SHORTCUT_LIST_SIZE_SIZE;
-                groupAddress += GROUP_SHORTCUT_LIST_SIZE_SIZE;
+                index += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
+                groupAddress += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
                 final Iterator<WeightedString> shortcutIterator = group.mShortcutTargets.iterator();
                 while (shortcutIterator.hasNext()) {
                     final WeightedString target = shortcutIterator.next();
@@ -921,6 +927,11 @@
             }
 
         }
+        if (formatOptions.mHasLinkedListNode) {
+            buffer[index] = buffer[index + 1] = buffer[index + 2]
+                    = FormatSpec.NO_FORWARD_LINK_ADDRESS;
+            index += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+        }
         if (index != node.mCachedAddress + node.mCachedSize) throw new RuntimeException(
                 "Not the same size : written "
                 + (index - node.mCachedAddress) + " bytes out of a node that should have "
@@ -990,10 +1001,10 @@
      *
      * @param destination the stream to write the binary data to.
      * @param dict the dictionary to write.
-     * @param version the version of the format to write, currently either 1 or 2.
+     * @param formatOptions file format options.
      */
     public static void writeDictionaryBinary(final OutputStream destination,
-            final FusionDictionary dict, final int version)
+            final FusionDictionary dict, final FormatOptions formatOptions)
             throws IOException, UnsupportedFormatException {
 
         // Addresses are limited to 3 bytes, but since addresses can be relative to each node, the
@@ -1002,36 +1013,39 @@
         // does not have a size limit, each node must still be within 16MB of all its children and
         // parents. As long as this is ensured, the dictionary file may grow to any size.
 
-        if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) {
+        final int version = formatOptions.mVersion;
+        if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
+                || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
             throw new UnsupportedFormatException("Requested file format version " + version
                     + ", but this implementation only supports versions "
-                    + MINIMUM_SUPPORTED_VERSION + " through " + MAXIMUM_SUPPORTED_VERSION);
+                    + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through "
+                    + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
         }
 
         ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256);
 
         // The magic number in big-endian order.
-        if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+        if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
             // Magic number for version 2+.
-            headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 24)));
-            headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 16)));
-            headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 8)));
-            headerBuffer.write((byte) (0xFF & VERSION_2_MAGIC_NUMBER));
+            headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 24)));
+            headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 16)));
+            headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 8)));
+            headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_2_MAGIC_NUMBER));
             // Dictionary version.
             headerBuffer.write((byte) (0xFF & (version >> 8)));
             headerBuffer.write((byte) (0xFF & version));
         } else {
             // Magic number for version 1.
-            headerBuffer.write((byte) (0xFF & (VERSION_1_MAGIC_NUMBER >> 8)));
-            headerBuffer.write((byte) (0xFF & VERSION_1_MAGIC_NUMBER));
+            headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_1_MAGIC_NUMBER >> 8)));
+            headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_1_MAGIC_NUMBER));
             // Dictionary version.
             headerBuffer.write((byte) (0xFF & version));
         }
         // Options flags
-        final int options = makeOptionsValue(dict);
+        final int options = makeOptionsValue(dict, formatOptions);
         headerBuffer.write((byte) (0xFF & (options >> 8)));
         headerBuffer.write((byte) (0xFF & options));
-        if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+        if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
             final int headerSizeOffset = headerBuffer.size();
             // Placeholder to be written later with header size.
             for (int i = 0; i < 4; ++i) {
@@ -1062,20 +1076,20 @@
         ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
 
         MakedictLog.i("Computing addresses...");
-        computeAddresses(dict, flatNodes);
+        computeAddresses(dict, flatNodes, formatOptions);
         MakedictLog.i("Checking array...");
         if (DBG) checkFlatNodeArray(flatNodes);
 
         // Create a buffer that matches the final dictionary size.
         final Node lastNode = flatNodes.get(flatNodes.size() - 1);
-        final int bufferSize =(lastNode.mCachedAddress + lastNode.mCachedSize);
+        final int bufferSize = lastNode.mCachedAddress + lastNode.mCachedSize;
         final byte[] buffer = new byte[bufferSize];
         int index = 0;
 
         MakedictLog.i("Writing file...");
         int dataEndOffset = 0;
         for (Node n : flatNodes) {
-            dataEndOffset = writePlacedNode(dict, buffer, n);
+            dataEndOffset = writePlacedNode(dict, buffer, n, formatOptions);
         }
 
         if (DBG) showStatistics(flatNodes);
@@ -1090,113 +1104,127 @@
     // Input methods: Read a binary dictionary to memory.
     // 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 final int[] CHARACTER_BUFFER = new int[FormatSpec.MAX_WORD_LENGTH];
+    public static CharGroupInfo readCharGroup(final FusionDictionaryBufferInterface buffer,
+            final int originalGroupAddress, final FormatOptions options) {
         int addressPointer = originalGroupAddress;
-        final int flags = source.readUnsignedByte();
+        final int flags = buffer.readUnsignedByte();
         ++addressPointer;
+
+        final int parentAddress;
+        if (hasParentAddress(options)) {
+            // read the parent address. (version 3)
+            parentAddress = -buffer.readUnsignedInt24();
+            addressPointer += 3;
+        } else {
+            parentAddress = FormatSpec.NO_PARENT_ADDRESS;
+        }
+
         final int characters[];
-        if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
+        if (0 != (flags & FormatSpec.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);
+                // FusionDictionary is making sure that the length of the word is smaller than
+                // MAX_WORD_LENGTH.
+                // So we'll never write past the end of CHARACTER_BUFFER.
+                CHARACTER_BUFFER[index++] = character;
+                character = CharEncoding.readChar(buffer);
                 addressPointer += CharEncoding.getCharSize(character);
             }
-            characters = Arrays.copyOfRange(characterBuffer, 0, index);
+            characters = Arrays.copyOfRange(CHARACTER_BUFFER, 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)) {
+        if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) {
             ++addressPointer;
-            frequency = source.readUnsignedByte();
+            frequency = buffer.readUnsignedByte();
         } else {
             frequency = CharGroup.NOT_A_TERMINAL;
         }
         int childrenAddress = addressPointer;
-        switch (flags & MASK_GROUP_ADDRESS_TYPE) {
-        case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
-            childrenAddress += source.readUnsignedByte();
+        switch (flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) {
+        case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
+            childrenAddress += buffer.readUnsignedByte();
             addressPointer += 1;
             break;
-        case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
-            childrenAddress += source.readUnsignedShort();
+        case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
+            childrenAddress += buffer.readUnsignedShort();
             addressPointer += 2;
             break;
-        case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
-            childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
+        case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
+            childrenAddress += buffer.readUnsignedInt24();
             addressPointer += 3;
             break;
-        case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
+        case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
         default:
-            childrenAddress = NO_CHILDREN_ADDRESS;
+            childrenAddress = FormatSpec.NO_CHILDREN_ADDRESS;
             break;
         }
         ArrayList<WeightedString> shortcutTargets = null;
-        if (0 != (flags & FLAG_HAS_SHORTCUT_TARGETS)) {
-            final long pointerBefore = source.getFilePointer();
+        if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) {
+            final int pointerBefore = buffer.position();
             shortcutTargets = new ArrayList<WeightedString>();
-            source.readUnsignedShort(); // Skip the size
+            buffer.readUnsignedShort(); // Skip the size
             while (true) {
-                final int targetFlags = source.readUnsignedByte();
-                final String word = CharEncoding.readString(source);
+                final int targetFlags = buffer.readUnsignedByte();
+                final String word = CharEncoding.readString(buffer);
                 shortcutTargets.add(new WeightedString(word,
-                        targetFlags & FLAG_ATTRIBUTE_FREQUENCY));
-                if (0 == (targetFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+                        targetFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY));
+                if (0 == (targetFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
             }
-            addressPointer += (source.getFilePointer() - pointerBefore);
+            addressPointer += buffer.position() - pointerBefore;
         }
         ArrayList<PendingAttribute> bigrams = null;
-        if (0 != (flags & FLAG_HAS_BIGRAMS)) {
+        if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
             bigrams = new ArrayList<PendingAttribute>();
             while (true) {
-                final int bigramFlags = source.readUnsignedByte();
+                final int bigramFlags = buffer.readUnsignedByte();
                 ++addressPointer;
-                final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
+                final int sign = 0 == (bigramFlags & FormatSpec.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();
+                switch (bigramFlags & FormatSpec.MASK_ATTRIBUTE_ADDRESS_TYPE) {
+                case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+                    bigramAddress += sign * buffer.readUnsignedByte();
                     addressPointer += 1;
                     break;
-                case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
-                    bigramAddress += sign * source.readUnsignedShort();
+                case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+                    bigramAddress += sign * buffer.readUnsignedShort();
                     addressPointer += 2;
                     break;
-                case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
-                    final int offset = ((source.readUnsignedByte() << 16)
-                            + source.readUnsignedShort());
+                case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+                    final int offset = (buffer.readUnsignedByte() << 16)
+                            + buffer.readUnsignedShort();
                     bigramAddress += sign * offset;
                     addressPointer += 3;
                     break;
                 default:
                     throw new RuntimeException("Has bigrams with no address");
                 }
-                bigrams.add(new PendingAttribute(bigramFlags & FLAG_ATTRIBUTE_FREQUENCY,
+                bigrams.add(new PendingAttribute(bigramFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY,
                         bigramAddress));
-                if (0 == (bigramFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+                if (0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
             }
         }
         return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
-                childrenAddress, shortcutTargets, bigrams);
+                parentAddress, childrenAddress, shortcutTargets, bigrams);
     }
 
     /**
-     * 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();
-        if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
+    public static int readCharGroupCount(final FusionDictionaryBufferInterface buffer) {
+        final int msb = buffer.readUnsignedByte();
+        if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
             return msb;
         } else {
-            return ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
-                    + source.readUnsignedByte();
+            return ((FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
+                    + buffer.readUnsignedByte();
         }
     }
 
@@ -1204,31 +1232,73 @@
     // 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.
+     * @param formatOptions file format options.
      * @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 {
+    /* packages for tests */ static String getWordAtAddress(
+            final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
+            final FormatOptions formatOptions) {
         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 String result;
+        final int originalPointer = buffer.position();
+
+        if (hasParentAddress(formatOptions)) {
+            result = getWordAtAddressWithParentAddress(buffer, headerSize, address, formatOptions);
+        } else {
+            result = getWordAtAddressWithoutParentAddress(buffer, headerSize, address,
+                    formatOptions);
+        }
+
+        wordCache.put(address, result);
+        buffer.position(originalPointer);
+        return result;
+    }
+
+    private static int[] sGetWordBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
+    private static String getWordAtAddressWithParentAddress(
+            final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
+            final FormatOptions options) {
+        final StringBuilder builder = new StringBuilder();
+
+        int currentAddress = address;
+        int index = FormatSpec.MAX_WORD_LENGTH - 1;
+        // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH
+        for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) {
+            buffer.position(currentAddress + headerSize);
+            final CharGroupInfo currentInfo = readCharGroup(buffer, currentAddress, options);
+            for (int i = 0; i < currentInfo.mCharacters.length; ++i) {
+                sGetWordBuffer[index--] =
+                        currentInfo.mCharacters[currentInfo.mCharacters.length - i - 1];
+            }
+
+            if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break;
+            currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
+        }
+
+        return new String(sGetWordBuffer, index + 1, FormatSpec.MAX_WORD_LENGTH - index - 1);
+    }
+
+    private static String getWordAtAddressWithoutParentAddress(
+            final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
+            final FormatOptions options) {
+        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, options);
             groupOffset = info.mEndAddress;
             if (info.mOriginalAddress == address) {
                 builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
@@ -1239,9 +1309,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 = buffer.readUnsignedByte();
                     last = null;
                     continue;
                 }
@@ -1249,67 +1319,90 @@
             }
             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 = buffer.readUnsignedByte();
                 last = null;
                 continue;
             }
         }
-        source.seek(originalPointer);
-        wordCache.put(address, result);
         return result;
     }
 
     /**
-     * Reads a single node from a binary file.
+     * Reads a single node from a buffer.
      *
-     * This methods reads the file at the current position of its file pointer. A node is
-     * fully expected to start at the current position.
+     * This methods reads the file at the current position. A node is fully expected to start at
+     * the current position.
      * 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.
+     * @param options file format options.
      * @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 FusionDictionaryBufferInterface buffer, final int headerSize,
+            final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap,
+            final FormatOptions options)
             throws IOException {
-        final int nodeOrigin = (int)(source.getFilePointer() - headerSize);
-        final int count = readCharGroupCount(source);
         final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
-        int groupOffset = nodeOrigin + getGroupCountSize(count);
-        for (int i = count; i > 0; --i) {
-            CharGroupInfo info = readCharGroup(source, 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);
-                    bigrams.add(new WeightedString(word, bigram.mFrequency));
+        final int nodeOrigin = buffer.position() - headerSize;
+
+        do { // Scan the linked-list node.
+            final int nodeHeadPosition = buffer.position() - headerSize;
+            final int count = readCharGroupCount(buffer);
+            int groupOffset = nodeHeadPosition + getGroupCountSize(count);
+            for (int i = count; i > 0; --i) { // Scan the array of CharGroup.
+                CharGroupInfo info = readCharGroup(buffer, groupOffset, options);
+                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(
+                                buffer, headerSize, bigram.mAddress, options);
+                        bigrams.add(new WeightedString(word, bigram.mFrequency));
+                    }
+                }
+                if (hasChildrenAddress(info.mChildrenAddress)) {
+                    Node children = reverseNodeMap.get(info.mChildrenAddress);
+                    if (null == children) {
+                        final int currentPosition = buffer.position();
+                        buffer.position(info.mChildrenAddress + headerSize);
+                        children = readNode(
+                                buffer, headerSize, reverseNodeMap, reverseGroupMap, options);
+                        buffer.position(currentPosition);
+                    }
+                    nodeContents.add(
+                            new CharGroup(info.mCharacters, shortcutTargets, bigrams,
+                                    info.mFrequency,
+                                    0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
+                                    0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children));
+                } else {
+                    nodeContents.add(
+                            new CharGroup(info.mCharacters, shortcutTargets, bigrams,
+                                    info.mFrequency,
+                                    0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
+                                    0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED)));
+                }
+                groupOffset = info.mEndAddress;
+            }
+
+            // reach the end of the array.
+            if (options.mHasLinkedListNode) {
+                final int nextAddress = buffer.readUnsignedInt24();
+                if (nextAddress >= 0 && nextAddress < buffer.limit()) {
+                    buffer.position(nextAddress);
+                } else {
+                    break;
                 }
             }
-            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);
-                }
-                nodeContents.add(
-                        new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency,
-                                children));
-            } else {
-                nodeContents.add(
-                        new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency));
-            }
-            groupOffset = info.mEndAddress;
-        }
+        } while (options.mHasLinkedListNode &&
+                buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
+
         final Node node = new Node(nodeContents);
         node.mCachedAddress = nodeOrigin;
         reverseNodeMap.put(node.mCachedAddress, node);
@@ -1318,65 +1411,117 @@
 
     /**
      * 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();
-        return NOT_A_VERSION_NUMBER;
+    private static int getFormatVersion(final FusionDictionaryBufferInterface buffer)
+            throws IOException {
+        final int magic_v1 = buffer.readUnsignedShort();
+        if (FormatSpec.VERSION_1_MAGIC_NUMBER == magic_v1) return buffer.readUnsignedByte();
+        final int magic_v2 = (magic_v1 << 16) + buffer.readUnsignedShort();
+        if (FormatSpec.VERSION_2_MAGIC_NUMBER == magic_v2) return buffer.readUnsignedShort();
+        return FormatSpec.NOT_A_VERSION_NUMBER;
     }
 
     /**
-     * Reads a random access file and returns the memory representation of the dictionary.
+     * Helper function to get and validate the binary format version.
+     * @throws UnsupportedFormatException
+     * @throws IOException
+     */
+    private static int checkFormatVersion(final FusionDictionaryBufferInterface buffer)
+            throws IOException, UnsupportedFormatException {
+        final int version = getFormatVersion(buffer);
+        if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
+                || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
+            throw new UnsupportedFormatException("This file has version " + version
+                    + ", but this implementation does not support versions above "
+                    + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
+        }
+        return version;
+    }
+
+    /**
+     * Reads a header from a buffer.
+     * @param buffer the buffer to read.
+     * @throws IOException
+     * @throws UnsupportedFormatException
+     */
+    public static FileHeader readHeader(final FusionDictionaryBufferInterface buffer)
+            throws IOException, UnsupportedFormatException {
+        final int version = checkFormatVersion(buffer);
+        final int optionsFlags = buffer.readUnsignedShort();
+
+        final HashMap<String, String> attributes = new HashMap<String, String>();
+        final int headerSize;
+        if (version < FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
+            headerSize = buffer.position();
+        } else {
+            headerSize = buffer.readInt();
+            populateOptions(buffer, headerSize, attributes);
+            buffer.position(headerSize);
+        }
+
+        if (headerSize < 0) {
+            throw new UnsupportedFormatException("header size can't be negative.");
+        }
+
+        final FileHeader header = new FileHeader(headerSize,
+                new FusionDictionary.DictionaryOptions(attributes,
+                        0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG),
+                        0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)),
+                new FormatOptions(version,
+                        0 != (optionsFlags & FormatSpec.HAS_PARENT_ADDRESS),
+                        0 != (optionsFlags & FormatSpec.HAS_LINKEDLIST_NODE)));
+        return header;
+    }
+
+    /**
+     * Reads options from a buffer and populate a map with their contents.
      *
-     * This high-level method takes a binary file and reads its contents, populating a
+     * The buffer is read at the current position, so the caller must take care the pointer
+     * is in the right place before calling this.
+     */
+    public static void populateOptions(final FusionDictionaryBufferInterface 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 buffer and returns the memory representation of the dictionary.
+     *
+     * This high-level method takes a buffer 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.
+     * which words from the buffer 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,
-            final FusionDictionary dict) throws IOException, UnsupportedFormatException {
-        // Check file version
-        final int version = getFormatVersion(source);
-        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);
-        }
+    public static FusionDictionary readDictionaryBinary(
+            final FusionDictionaryBufferInterface buffer, final FusionDictionary dict)
+                    throws IOException, UnsupportedFormatException {
+        // clear cache
+        wordCache.clear();
 
-        // Read options
-        final int optionsFlags = source.readUnsignedShort();
-
-        final long headerSize;
-        final HashMap<String, String> options = new HashMap<String, String>();
-        if (version < FIRST_VERSION_WITH_HEADER_SIZE) {
-            headerSize = source.getFilePointer();
-        } 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);
-        }
+        // Read header
+        final FileHeader header = readHeader(buffer);
 
         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, header.mHeaderSize, reverseNodeMapping,
+                reverseGroupMapping, header.mFormatOptions);
 
-        FusionDictionary newDict = new FusionDictionary(root,
-                new FusionDictionary.DictionaryOptions(options,
-                        0 != (optionsFlags & GERMAN_UMLAUT_PROCESSING_FLAG),
-                        0 != (optionsFlags & FRENCH_LIGATURE_PROCESSING_FLAG)));
+        FusionDictionary newDict = new FusionDictionary(root, header.mDictionaryOptions);
         if (null != dict) {
             for (final Word w : dict) {
-                newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets);
+                if (w.mIsBlacklistEntry) {
+                    newDict.addBlacklistEntry(w.mWord, w.mShortcutTargets, w.mIsNotAWord);
+                } else {
+                    newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets, w.mIsNotAWord);
+                }
             }
             for (final Word w : dict) {
                 // By construction a binary dictionary may not have bigrams pointing to
@@ -1400,14 +1545,45 @@
      * @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);
-            return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION);
+            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(new ByteBufferWrapper(buffer));
+            return (version >= FormatSpec.MINIMUM_SUPPORTED_VERSION
+                    && version <= FormatSpec.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 = (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency)
+                / (1.5f + FormatSpec.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/CharGroupInfo.java b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
index ef7dbb2..ed93884 100644
--- a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
+++ b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
@@ -31,18 +31,20 @@
     public final int[] mCharacters;
     public final int mFrequency;
     public final int mChildrenAddress;
+    public final int mParentAddress;
     public final ArrayList<WeightedString> mShortcutTargets;
     public final ArrayList<PendingAttribute> mBigrams;
 
     public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
-            final int[] characters, final int frequency, final int childrenAddress,
-            final ArrayList<WeightedString> shortcutTargets,
+            final int[] characters, final int frequency, final int parentAddress,
+            final int childrenAddress, final ArrayList<WeightedString> shortcutTargets,
             final ArrayList<PendingAttribute> bigrams) {
         mOriginalAddress = originalAddress;
         mEndAddress = endAddress;
         mFlags = flags;
         mCharacters = characters;
         mFrequency = frequency;
+        mParentAddress = parentAddress;
         mChildrenAddress = childrenAddress;
         mShortcutTargets = shortcutTargets;
         mBigrams = bigrams;
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
new file mode 100644
index 0000000..adc6037
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -0,0 +1,264 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
+
+/**
+ * Dictionary File Format Specification.
+ */
+public final class FormatSpec {
+
+    /*
+     * Array of Node(FusionDictionary.Node) layout is as follows:
+     *
+     * g |
+     * r | the number of groups, 1 or 2 bytes.
+     * o | 1 byte = bbbbbbbb match
+     * u |   case 1xxxxxxx => xxxxxxx << 8 + next byte
+     * p |   otherwise => bbbbbbbb
+     * c |
+     * ount
+     *
+     * g |
+     * r | sequence of groups,
+     * o | the layout of each group is described below.
+     * u |
+     * ps
+     *
+     * f |
+     * o | IF HAS_LINKEDLIST_NODE (defined in the file header)
+     * r |     forward link address, 3byte
+     * w | the address must be positive.
+     * a |
+     * rdlinkaddress
+     */
+
+    /* Node(CharGroup) layout is as follows:
+     *   | addressType                         xx     : mask with MASK_GROUP_ADDRESS_TYPE
+     *                                 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
+     * f |                                     01 = 1 byte      : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
+     * l |                                     10 = 2 bytes     : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
+     * a |                                     11 = 3 bytes     : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
+     * g | has several chars ?         1 bit, 1 = yes, 0 = no   : FLAG_HAS_MULTIPLE_CHARS
+     * s | has a terminal ?            1 bit, 1 = yes, 0 = no   : FLAG_IS_TERMINAL
+     *   | has shortcut targets ?      1 bit, 1 = yes, 0 = no   : FLAG_HAS_SHORTCUT_TARGETS
+     *   | has bigrams ?               1 bit, 1 = yes, 0 = no   : FLAG_HAS_BIGRAMS
+     *   | is not a word ?             1 bit, 1 = yes, 0 = no   : FLAG_IS_NOT_A_WORD
+     *   | is blacklisted ?            1 bit, 1 = yes, 0 = no   : FLAG_IS_BLACKLISTED
+     *
+     * p |
+     * a | IF HAS_PARENT_ADDRESS (defined in the file header)
+     * r |     parent address, 3byte
+     * e | the address must be negative, so the absolute value of the address is stored.
+     * n |
+     * taddress
+     *
+     * c | IF FLAG_HAS_MULTIPLE_CHARS
+     * h |   char, char, char, char    n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+     * a |   end                       1 byte, = 0
+     * r | ELSE
+     * s |   char                      1 or 3 bytes
+     *   | END
+     *
+     * f |
+     * r | IF FLAG_IS_TERMINAL
+     * e |   frequency                 1 byte
+     * q |
+     *
+     * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
+     * h |   // nothing
+     * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
+     * l |   children address, 1 byte
+     * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
+     * r |   children address, 2 bytes
+     * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
+     * n |   children address, 3 bytes
+     * A | END
+     * d
+     * dress
+     *
+     *   | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
+     *   | shortcut string list
+     *   | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
+     *   | bigrams address list
+     *
+     * Char format is:
+     * 1 byte = bbbbbbbb match
+     * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+     * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+     *       unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+     *       00011111 would be outside unicode.
+     * else: iso-latin-1 code
+     * This allows for the whole unicode range to be encoded, including chars outside of
+     * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+     * characters which should never happen anyway (and still work, but take 3 bytes).
+     *
+     * bigram address list is:
+     * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no     : FLAG_ATTRIBUTE_HAS_NEXT
+     *           | addressSign = 1 bit,                 : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
+     *           |                      1 = must take -address, 0 = must take +address
+     *           |                         xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
+     *           | addressFormat = 2 bits, 00 = unused  : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+     *           |                         01 = 1 byte  : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+     *           |                         10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
+     *           |                         11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
+     *           | 4 bits : frequency         : mask with FLAG_ATTRIBUTE_FREQUENCY
+     * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
+     *           |   read 1 byte, add top 4 bits
+     *           | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+     *           |   read 2 bytes, add top 4 bits
+     *           | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
+     *           |   read 3 bytes, add top 4 bits
+     *           | END
+     *           | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
+     * if (FLAG_ATTRIBUTE_HAS_NEXT) goto bigram_and_shortcut_address_list_is
+     *
+     * shortcut string list is:
+     * <byte size> = GROUP_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes.
+     * <flags>     = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+     *               | reserved = 3 bits, must be 0
+     *               | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
+     * <shortcut>  = | string of characters at the char format described above, with the terminator
+     *               | used to signal the end of the string.
+     * if (FLAG_ATTRIBUTE_HAS_NEXT goto flags
+     */
+
+    static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
+    public static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
+    static final int MINIMUM_SUPPORTED_VERSION = 1;
+    static final int MAXIMUM_SUPPORTED_VERSION = 3;
+    static final int NOT_A_VERSION_NUMBER = -1;
+    static final int FIRST_VERSION_WITH_HEADER_SIZE = 2;
+    static final int FIRST_VERSION_WITH_PARENT_ADDRESS = 3;
+    static final int FIRST_VERSION_WITH_LINKEDLIST_NODE = 3;
+
+    // These options need to be the same numeric values as the one in the native reading code.
+    static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
+    // TODO: Make the native reading code read this variable.
+    static final int HAS_PARENT_ADDRESS = 0x2;
+    static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4;
+    static final int CONTAINS_BIGRAMS_FLAG = 0x8;
+    // TODO: Make the native reading code read this variable.
+    static final int HAS_LINKEDLIST_NODE = 0x10;
+
+    // TODO: Make this value adaptative to content data, store it in the header, and
+    // use it in the reading code.
+    static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
+
+    static final int PARENT_ADDRESS_SIZE = 3;
+    static final int FORWARD_LINK_ADDRESS_SIZE = 3;
+
+    static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
+    static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
+    static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
+    static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
+    static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+
+    static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
+
+    static final int FLAG_IS_TERMINAL = 0x10;
+    static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
+    static final int FLAG_HAS_BIGRAMS = 0x04;
+    static final int FLAG_IS_NOT_A_WORD = 0x02;
+    static final int FLAG_IS_BLACKLISTED = 0x01;
+
+    static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
+    static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
+    static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
+    static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
+    static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
+    static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
+    static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
+
+    static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
+
+    static final int GROUP_TERMINATOR_SIZE = 1;
+    static final int GROUP_FLAGS_SIZE = 1;
+    static final int GROUP_FREQUENCY_SIZE = 1;
+    static final int GROUP_MAX_ADDRESS_SIZE = 3;
+    static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
+    static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
+    static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2;
+
+    static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
+    static final int NO_PARENT_ADDRESS = 0;
+    static final int NO_FORWARD_LINK_ADDRESS = 0;
+    static final int INVALID_CHARACTER = -1;
+
+    static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127
+    static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767
+
+    static final int MAX_TERMINAL_FREQUENCY = 255;
+    static final int MAX_BIGRAM_FREQUENCY = 15;
+
+    // This option needs to be the same numeric value as the one in binary_format.h.
+    static final int NOT_VALID_WORD = -99;
+
+    /**
+     * Options about file format.
+     */
+    public static class FormatOptions {
+        public final int mVersion;
+        public final boolean mHasParentAddress;
+        public final boolean mHasLinkedListNode;
+        public FormatOptions(final int version) {
+            this(version, false);
+        }
+        public FormatOptions(final int version, final boolean hasParentAddress) {
+            this(version, hasParentAddress, false);
+        }
+        public FormatOptions(final int version, final boolean hasParentAddress,
+                final boolean hasLinkedListNode) {
+            mVersion = version;
+            if (version < FIRST_VERSION_WITH_PARENT_ADDRESS && hasParentAddress) {
+                throw new RuntimeException("Parent addresses are only supported with versions "
+                        + FIRST_VERSION_WITH_PARENT_ADDRESS + " and ulterior.");
+            }
+            mHasParentAddress = hasParentAddress;
+
+            if (version < FIRST_VERSION_WITH_LINKEDLIST_NODE && hasLinkedListNode) {
+                throw new RuntimeException("Linked list nodes are only supported with versions "
+                        + FIRST_VERSION_WITH_LINKEDLIST_NODE + " and ulterior.");
+            }
+            if (!hasParentAddress && hasLinkedListNode) {
+                throw new RuntimeException("Linked list nodes need parent addresses.");
+            }
+            mHasLinkedListNode = hasLinkedListNode;
+        }
+    }
+
+    /**
+     * Class representing file header.
+     */
+    static final class FileHeader {
+        public final int mHeaderSize;
+        public final DictionaryOptions mDictionaryOptions;
+        public final FormatOptions mFormatOptions;
+        public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions,
+                final FormatOptions formatOptions) {
+            mHeaderSize = headerSize;
+            mDictionaryOptions = dictionaryOptions;
+            mFormatOptions = formatOptions;
+        }
+    }
+
+    private FormatSpec() {
+        // This utility class is not publicly instantiable.
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 8b53c94..98cf308 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.makedict;
 
+import com.android.inputmethod.latin.Constants;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -41,17 +43,15 @@
     public static class Node {
         ArrayList<CharGroup> mData;
         // To help with binary generation
-        int mCachedSize;
-        int mCachedAddress;
+        int mCachedSize = Integer.MIN_VALUE;
+        int mCachedAddress = Integer.MIN_VALUE;
+        int mCachedParentAddress = 0;
+
         public Node() {
             mData = new ArrayList<CharGroup>();
-            mCachedSize = Integer.MIN_VALUE;
-            mCachedAddress = Integer.MIN_VALUE;
         }
         public Node(ArrayList<CharGroup> data) {
             mData = data;
-            mCachedSize = Integer.MIN_VALUE;
-            mCachedAddress = Integer.MIN_VALUE;
         }
     }
 
@@ -61,8 +61,8 @@
      * This represents an "attribute", that is either a bigram or a shortcut.
      */
     public static class WeightedString {
-        final String mWord;
-        int mFrequency;
+        public final String mWord;
+        public int mFrequency;
         public WeightedString(String word, int frequency) {
             mWord = word;
             mFrequency = frequency;
@@ -101,26 +101,34 @@
         ArrayList<WeightedString> mBigrams;
         int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
         Node mChildren;
+        boolean mIsNotAWord; // Only a shortcut
+        boolean mIsBlacklistEntry;
         // The two following members to help with binary generation
         int mCachedSize;
         int mCachedAddress;
 
         public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
-                final ArrayList<WeightedString> bigrams, final int frequency) {
+                final ArrayList<WeightedString> bigrams, final int frequency,
+                final boolean isNotAWord, final boolean isBlacklistEntry) {
             mChars = chars;
             mFrequency = frequency;
             mShortcutTargets = shortcutTargets;
             mBigrams = bigrams;
             mChildren = null;
+            mIsNotAWord = isNotAWord;
+            mIsBlacklistEntry = isBlacklistEntry;
         }
 
         public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
-                final ArrayList<WeightedString> bigrams, final int frequency, final Node children) {
+                final ArrayList<WeightedString> bigrams, final int frequency,
+                final boolean isNotAWord, final boolean isBlacklistEntry, final Node children) {
             mChars = chars;
             mFrequency = frequency;
             mShortcutTargets = shortcutTargets;
             mBigrams = bigrams;
             mChildren = children;
+            mIsNotAWord = isNotAWord;
+            mIsBlacklistEntry = isBlacklistEntry;
         }
 
         public void addChild(CharGroup n) {
@@ -197,8 +205,9 @@
          * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only
          * updated if they are higher than the existing ones.
          */
-        public void update(int frequency, ArrayList<WeightedString> shortcutTargets,
-                ArrayList<WeightedString> bigrams) {
+        public void update(final int frequency, final ArrayList<WeightedString> shortcutTargets,
+                final ArrayList<WeightedString> bigrams,
+                final boolean isNotAWord, final boolean isBlacklistEntry) {
             if (frequency > mFrequency) {
                 mFrequency = frequency;
             }
@@ -234,6 +243,8 @@
                     }
                 }
             }
+            mIsNotAWord = isNotAWord;
+            mIsBlacklistEntry = isBlacklistEntry;
         }
     }
 
@@ -296,10 +307,24 @@
      * @param word the word to add.
      * @param frequency the frequency of the word, in the range [0..255].
      * @param shortcutTargets a list of shortcut targets for this word, or null.
+     * @param isNotAWord true if this should not be considered a word (e.g. shortcut only)
      */
     public void add(final String word, final int frequency,
-            final ArrayList<WeightedString> shortcutTargets) {
-        add(getCodePoints(word), frequency, shortcutTargets);
+            final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) {
+        add(getCodePoints(word), frequency, shortcutTargets, isNotAWord,
+                false /* isBlacklistEntry */);
+    }
+
+    /**
+     * Helper method to add a blacklist entry as a string.
+     *
+     * @param word the word to add as a blacklist entry.
+     * @param shortcutTargets a list of shortcut targets for this word, or null.
+     * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so)
+     */
+    public void addBlacklistEntry(final String word,
+            final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) {
+        add(getCodePoints(word), 0, shortcutTargets, isNotAWord, true /* isBlacklistEntry */);
     }
 
     /**
@@ -332,7 +357,8 @@
         if (charGroup != null) {
             final CharGroup charGroup2 = findWordInTree(mRoot, word2);
             if (charGroup2 == null) {
-                add(getCodePoints(word2), 0, null);
+                add(getCodePoints(word2), 0, null, false /* isNotAWord */,
+                        false /* isBlacklistEntry */);
             }
             charGroup.addBigram(word2, frequency);
         } else {
@@ -349,10 +375,18 @@
      * @param word the word, as an int array.
      * @param frequency the frequency of the word, in the range [0..255].
      * @param shortcutTargets an optional list of shortcut targets for this word (null if none).
+     * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so)
+     * @param isBlacklistEntry true if this is a blacklisted word, false otherwise
      */
     private void add(final int[] word, final int frequency,
-            final ArrayList<WeightedString> shortcutTargets) {
+            final ArrayList<WeightedString> shortcutTargets,
+            final boolean isNotAWord, final boolean isBlacklistEntry) {
         assert(frequency >= 0 && frequency <= 255);
+        if (word.length >= Constants.Dictionary.MAX_WORD_LENGTH) {
+            MakedictLog.w("Ignoring a word that is too long: word.length = " + word.length);
+            return;
+        }
+
         Node currentNode = mRoot;
         int charIndex = 0;
 
@@ -376,7 +410,7 @@
             final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
             final CharGroup newGroup = new CharGroup(
                     Arrays.copyOfRange(word, charIndex, word.length),
-                    shortcutTargets, null /* bigrams */, frequency);
+                    shortcutTargets, null /* bigrams */, frequency, isNotAWord, isBlacklistEntry);
             currentNode.mData.add(insertionIndex, newGroup);
             if (DBG) checkStack(currentNode);
         } else {
@@ -386,13 +420,15 @@
                     // The new word is a prefix of an existing word, but the node on which it
                     // should end already exists as is. Since the old CharNode was not a terminal, 
                     // make it one by filling in its frequency and other attributes
-                    currentGroup.update(frequency, shortcutTargets, null);
+                    currentGroup.update(frequency, shortcutTargets, null, isNotAWord,
+                            isBlacklistEntry);
                 } else {
                     // The new word matches the full old word and extends past it.
                     // We only have to create a new node and add it to the end of this.
                     final CharGroup newNode = new CharGroup(
                             Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
-                                    shortcutTargets, null /* bigrams */, frequency);
+                                    shortcutTargets, null /* bigrams */, frequency, isNotAWord,
+                                    isBlacklistEntry);
                     currentGroup.mChildren = new Node();
                     currentGroup.mChildren.mData.add(newNode);
                 }
@@ -400,7 +436,9 @@
                 if (0 == differentCharIndex) {
                     // Exact same word. Update the frequency if higher. This will also add the
                     // new shortcuts to the existing shortcut list if it already exists.
-                    currentGroup.update(frequency, shortcutTargets, null);
+                    currentGroup.update(frequency, shortcutTargets, null,
+                            currentGroup.mIsNotAWord && isNotAWord,
+                            currentGroup.mIsBlacklistEntry || isBlacklistEntry);
                 } else {
                     // Partial prefix match only. We have to replace the current node with a node
                     // containing the current prefix and create two new ones for the tails.
@@ -408,21 +446,26 @@
                     final CharGroup newOldWord = new CharGroup(
                             Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
                                     currentGroup.mChars.length), currentGroup.mShortcutTargets,
-                            currentGroup.mBigrams, currentGroup.mFrequency, currentGroup.mChildren);
+                            currentGroup.mBigrams, currentGroup.mFrequency,
+                            currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry,
+                            currentGroup.mChildren);
                     newChildren.mData.add(newOldWord);
 
                     final CharGroup newParent;
                     if (charIndex + differentCharIndex >= word.length) {
                         newParent = new CharGroup(
                                 Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
-                                shortcutTargets, null /* bigrams */, frequency, newChildren);
+                                shortcutTargets, null /* bigrams */, frequency,
+                                isNotAWord, isBlacklistEntry, newChildren);
                     } else {
                         newParent = new CharGroup(
                                 Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
-                                null /* shortcutTargets */, null /* bigrams */, -1, newChildren);
+                                null /* shortcutTargets */, null /* bigrams */, -1, 
+                                false /* isNotAWord */, false /* isBlacklistEntry */, newChildren);
                         final CharGroup newWord = new CharGroup(Arrays.copyOfRange(word,
                                 charIndex + differentCharIndex, word.length),
-                                shortcutTargets, null /* bigrams */, frequency);
+                                shortcutTargets, null /* bigrams */, frequency,
+                                isNotAWord, isBlacklistEntry);
                         final int addIndex = word[charIndex + differentCharIndex]
                                 > currentGroup.mChars[differentCharIndex] ? 1 : 0;
                         newChildren.mData.add(addIndex, newWord);
@@ -483,7 +526,8 @@
     private static int findInsertionIndex(final Node node, int character) {
         final ArrayList<CharGroup> data = node.mData;
         final CharGroup reference = new CharGroup(new int[] { character },
-                null /* shortcutTargets */, null /* bigrams */, 0);
+                null /* shortcutTargets */, null /* bigrams */, 0, false /* isNotAWord */,
+                false /* isBlacklistEntry */);
         int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR);
         return result >= 0 ? result : -result - 1;
     }
@@ -512,17 +556,28 @@
         final StringBuilder checker = DBG ? new StringBuilder() : null;
 
         CharGroup currentGroup;
+        final int codePointCountInS = s.codePointCount(0, s.length());
         do {
             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()) {
+            if (index < codePointCountInS) {
                 node = currentGroup.mChildren;
             }
-        } while (null != node && index < s.length());
+        } while (null != node && index < codePointCountInS);
 
+        if (index < codePointCountInS) return null;
+        if (!currentGroup.isTerminal()) return null;
         if (DBG && !s.equals(checker.toString())) return null;
         return currentGroup;
     }
@@ -679,7 +734,7 @@
 //      StringBuilder s = new StringBuilder();
 //      for (CharGroup g : node.data) {
 //          s.append(g.frequency);
-//          for (int ch : g.chars){
+//          for (int ch : g.chars) {
 //              s.append(Character.toChars(ch));
 //          }
 //      }
@@ -738,13 +793,14 @@
                     }
                     if (currentGroup.mFrequency >= 0)
                         return new Word(mCurrentString.toString(), currentGroup.mFrequency,
-                                currentGroup.mShortcutTargets, currentGroup.mBigrams);
+                                currentGroup.mShortcutTargets, currentGroup.mBigrams,
+                                currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry);
                 } else {
                     mPositions.removeLast();
                     currentPos = mPositions.getLast();
                     mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length);
                 }
-            } while(true);
+            } while (true);
         }
 
         @Override
diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java
index d078267..4683ef1 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Word.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Word.java
@@ -27,20 +27,25 @@
  * This is chiefly used to iterate a dictionary.
  */
 public class Word implements Comparable<Word> {
-    final String mWord;
-    final int mFrequency;
-    final ArrayList<WeightedString> mShortcutTargets;
-    final ArrayList<WeightedString> mBigrams;
+    public final String mWord;
+    public final int mFrequency;
+    public final ArrayList<WeightedString> mShortcutTargets;
+    public final ArrayList<WeightedString> mBigrams;
+    public final boolean mIsNotAWord;
+    public final boolean mIsBlacklistEntry;
 
     private int mHashCode = 0;
 
     public Word(final String word, final int frequency,
             final ArrayList<WeightedString> shortcutTargets,
-            final ArrayList<WeightedString> bigrams) {
+            final ArrayList<WeightedString> bigrams,
+            final boolean isNotAWord, final boolean isBlacklistEntry) {
         mWord = word;
         mFrequency = frequency;
         mShortcutTargets = shortcutTargets;
         mBigrams = bigrams;
+        mIsNotAWord = isNotAWord;
+        mIsBlacklistEntry = isBlacklistEntry;
     }
 
     private static int computeHashCode(Word word) {
@@ -48,7 +53,9 @@
                 word.mWord,
                 word.mFrequency,
                 word.mShortcutTargets.hashCode(),
-                word.mBigrams.hashCode()
+                word.mBigrams.hashCode(),
+                word.mIsNotAWord,
+                word.mIsBlacklistEntry
         });
     }
 
@@ -78,7 +85,9 @@
         Word w = (Word)o;
         return mFrequency == w.mFrequency && mWord.equals(w.mWord)
                 && mShortcutTargets.equals(w.mShortcutTargets)
-                && mBigrams.equals(w.mBigrams);
+                && mBigrams.equals(w.mBigrams)
+                && mIsNotAWord == w.mIsNotAWord
+                && mIsBlacklistEntry == w.mIsBlacklistEntry;
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index ba974ff..eef7a51 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -16,37 +16,26 @@
 
 package com.android.inputmethod.latin.spellcheck;
 
-import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.database.ContentObserver;
 import android.preference.PreferenceManager;
-import android.provider.UserDictionary.Words;
 import android.service.textservice.SpellCheckerService;
-import android.text.TextUtils;
 import android.util.Log;
-import android.util.LruCache;
-import android.view.textservice.SentenceSuggestionsInfo;
 import android.view.textservice.SuggestionsInfo;
-import android.view.textservice.TextInfo;
 
-import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
 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.Dictionary.WordCallback;
 import com.android.inputmethod.latin.DictionaryCollection;
 import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.LocaleUtils;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StringUtils;
 import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary;
-import com.android.inputmethod.latin.SynchronouslyLoadedContactsDictionary;
 import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
-import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
-import com.android.inputmethod.latin.WhitelistDictionary;
-import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.UserBinaryDictionary;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -69,18 +58,15 @@
 
     public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
 
-    private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
-    private static final int CAPITALIZE_FIRST = 1; // First only
-    private static final int CAPITALIZE_ALL = 2; // All caps
+    public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
+    public static final int CAPITALIZE_FIRST = 1; // First only
+    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, Dictionary> mUserDictionaries =
-            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
-    private Map<String, Dictionary> mWhitelistDictionaries =
-            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
-    private Dictionary mContactsDictionary;
+    private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
+    private Map<String, UserBinaryDictionary> mUserDictionaries =
+            CollectionUtils.newSynchronizedTreeMap();
+    private ContactsBinaryDictionary mContactsDictionary;
 
     // The threshold for a candidate to be offered as a suggestion.
     private float mSuggestionThreshold;
@@ -91,12 +77,12 @@
     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;
-    private static final String SINGLE_QUOTE = "\u0027";
-    private static final String APOSTROPHE = "\u2019";
+    public static final String SINGLE_QUOTE = "\u0027";
+    public static final String APOSTROPHE = "\u2019";
     private static final TreeMap<String, Integer> mLanguageToScript;
     static {
         // List of the supported languages and their associated script. We won't check
@@ -107,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);
@@ -133,7 +119,7 @@
         onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
     }
 
-    private static int getScriptFromLocale(final Locale locale) {
+    public static int getScriptFromLocale(final Locale locale) {
         final Integer script = mLanguageToScript.get(locale.getLanguage());
         if (null == script) {
             throw new RuntimeException("We have been called with an unsupported language: \""
@@ -157,13 +143,9 @@
 
     private void startUsingContactsDictionaryLocked() {
         if (null == mContactsDictionary) {
-            if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
-                // TODO: use the right locale for each session
-                mContactsDictionary =
-                        new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
-            } else {
-                mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
-            }
+            // TODO: use the right locale for each session
+            mContactsDictionary =
+                    new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
         }
         final Iterator<WeakReference<DictionaryCollection>> iterator =
                 mDictionaryCollectionsList.iterator();
@@ -199,19 +181,27 @@
 
     @Override
     public Session createSession() {
-        return new AndroidSpellCheckerSession(this);
+        // Should not refer to AndroidSpellCheckerSession directly considering
+        // that AndroidSpellCheckerSession may be overlaid.
+        return AndroidSpellCheckerSessionFactory.newInstance(this);
     }
 
-    private static SuggestionsInfo getNotInDictEmptySuggestions() {
+    public static SuggestionsInfo getNotInDictEmptySuggestions() {
         return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
     }
 
-    private static SuggestionsInfo getInDictEmptySuggestions() {
+    public static SuggestionsInfo getInDictEmptySuggestions() {
         return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
                 EMPTY_STRING_ARRAY);
     }
 
-    private static class SuggestionsGatherer implements WordCallback {
+    public SuggestionsGatherer newSuggestionsGatherer(final String text, int maxLength) {
+        return new SuggestionsGatherer(
+                text, mSuggestionThreshold, mRecommendedThreshold, maxLength);
+    }
+
+    // TODO: remove this class and replace it by storage local to the session.
+    public static class SuggestionsGatherer {
         public static class Result {
             public final String[] mSuggestions;
             public final boolean mHasRecommendedSuggestions;
@@ -241,13 +231,12 @@
             mSuggestionThreshold = suggestionThreshold;
             mRecommendedThreshold = recommendedThreshold;
             mMaxLength = maxLength;
-            mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
+            mSuggestions = CollectionUtils.newArrayList(maxLength + 1);
             mScores = new int[mMaxLength];
         }
 
-        @Override
-        synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
-                int dicTypeId, int dataType) {
+        synchronized public boolean addWord(char[] word, int[] spaceIndices, int wordOffset,
+                int wordLength, int score) {
             final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score);
             // binarySearch returns the index if the element exists, and -<insertion index> - 1
             // if it doesn't. See documentation for binarySearch.
@@ -370,11 +359,9 @@
 
     private void closeAllDictionaries() {
         final Map<String, DictionaryPool> oldPools = mDictionaryPools;
-        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
-        final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
-        mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
-        final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
-        mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+        mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
+        final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries;
+        mUserDictionaries = CollectionUtils.newSynchronizedTreeMap();
         new Thread("spellchecker_close_dicts") {
             @Override
             public void run() {
@@ -384,15 +371,12 @@
                 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
                         // or several pools, but it is shielded against multiple closing and it's
                         // safe to call it several times.
-                        final Dictionary dictToClose = mContactsDictionary;
+                        final ContactsBinaryDictionary dictToClose = mContactsDictionary;
                         // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY
                         // is no longer needed
                         mContactsDictionary = null;
@@ -403,7 +387,7 @@
         }.start();
     }
 
-    private DictionaryPool getDictionaryPool(final String locale) {
+    public DictionaryPool getDictionaryPool(final String locale) {
         DictionaryPool pool = mDictionaryPools.get(locale);
         if (null == pool) {
             final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
@@ -424,36 +408,20 @@
                 DictionaryFactory.createMainDictionaryFromManager(this, locale,
                         true /* useFullEditDistance */);
         final String localeStr = locale.toString();
-        Dictionary userDictionary = mUserDictionaries.get(localeStr);
+        UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr);
         if (null == userDictionary) {
-            if (LatinIME.USE_BINARY_USER_DICTIONARY) {
-                userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
-            } else {
-                userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
-            }
+            userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
             mUserDictionaries.put(localeStr, userDictionary);
         }
         dictionaryCollection.addDictionary(userDictionary);
-        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) {
-                    // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
-                    // longer needed
-                    if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
-                        // TODO: use the right locale. We can't do it right now because the
-                        // spell checker is reusing the contacts dictionary across sessions
-                        // without regard for their locale, so we need to fix that first.
-                        mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
-                                Locale.getDefault());
-                    } else {
-                        mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
-                    }
+                    // TODO: use the right locale. We can't do it right now because the
+                    // spell checker is reusing the contacts dictionary across sessions
+                    // without regard for their locale, so we need to fix that first.
+                    mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
+                            Locale.getDefault());
                 }
             }
             dictionaryCollection.addDictionary(mContactsDictionary);
@@ -464,7 +432,7 @@
     }
 
     // This method assumes the text is not empty or null.
-    private static int getCapitalizationType(String text) {
+    public static int getCapitalizationType(String text) {
         // If the first char is not uppercase, then the word is either all lower case,
         // and in either case we return CAPITALIZE_NONE.
         if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE;
@@ -481,378 +449,4 @@
         if (1 == capsCount) return CAPITALIZE_FIRST;
         return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
     }
-
-    private static class AndroidSpellCheckerSession extends Session {
-        // Immutable, but need the locale which is not available in the constructor yet
-        private DictionaryPool mDictionaryPool;
-        // Likewise
-        private Locale mLocale;
-        // Cache this for performance
-        private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
-
-        private final AndroidSpellCheckerService mService;
-
-        private final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
-        private final ContentObserver mObserver;
-
-        private static class SuggestionsParams {
-            public final String[] mSuggestions;
-            public final int mFlags;
-            public SuggestionsParams(String[] suggestions, int flags) {
-                mSuggestions = suggestions;
-                mFlags = flags;
-            }
-        }
-
-        private static class SuggestionsCache {
-            private static final int MAX_CACHE_SIZE = 50;
-            // TODO: support bigram
-            private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
-                    new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
-
-            public SuggestionsParams getSuggestionsFromCache(String query) {
-                return mUnigramSuggestionsInfoCache.get(query);
-            }
-
-            public void putSuggestionsToCache(String query, String[] suggestions, int flags) {
-                if (suggestions == null || TextUtils.isEmpty(query)) {
-                    return;
-                }
-                mUnigramSuggestionsInfoCache.put(query, new SuggestionsParams(suggestions, flags));
-            }
-
-            public void clearCache() {
-                mUnigramSuggestionsInfoCache.evictAll();
-            }
-        }
-
-        AndroidSpellCheckerSession(final AndroidSpellCheckerService service) {
-            mService = service;
-            final ContentResolver cres = service.getContentResolver();
-
-            mObserver = new ContentObserver(null) {
-                @Override
-                public void onChange(boolean self) {
-                    mSuggestionsCache.clearCache();
-                }
-            };
-            cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
-        }
-
-        @Override
-        public void onCreate() {
-            final String localeString = getLocale();
-            mDictionaryPool = mService.getDictionaryPool(localeString);
-            mLocale = LocaleUtils.constructLocaleFromString(localeString);
-            mScript = getScriptFromLocale(mLocale);
-        }
-
-        @Override
-        public void onClose() {
-            final ContentResolver cres = mService.getContentResolver();
-            cres.unregisterContentObserver(mObserver);
-        }
-
-        /*
-         * Returns whether the code point is a letter that makes sense for the specified
-         * locale for this spell checker.
-         * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
-         * and is limited to EFIGS languages and Russian.
-         * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
-         * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
-         */
-        private static boolean isLetterCheckableByLanguage(final int codePoint,
-                final int script) {
-            switch (script) {
-            case SCRIPT_LATIN:
-                // Our supported latin script dictionaries (EFIGS) at the moment only include
-                // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
-                // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
-                // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
-                // excluded from isLetter anyway.
-                return codePoint <= 0x2AF && Character.isLetter(codePoint);
-            case SCRIPT_CYRILLIC:
-                // All Cyrillic characters are in the 400~52F block. There are some in the upper
-                // Unicode range, but they are archaic characters that are not used in modern
-                // russian and are not used by our dictionary.
-                return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
-            default:
-                // Should never come here
-                throw new RuntimeException("Impossible value of script: " + script);
-            }
-        }
-
-        /**
-         * Finds out whether a particular string should be filtered out of spell checking.
-         *
-         * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
-         * we know we will never recognize, this accepts a script identifier that should be one
-         * of the SCRIPT_* constants defined above, to rule out quickly characters from very
-         * different languages.
-         *
-         * @param text the string to evaluate.
-         * @param script the identifier for the script this spell checker recognizes
-         * @return true if we should filter this text out, false otherwise
-         */
-        private static boolean shouldFilterOut(final String text, final int script) {
-            if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
-
-            // TODO: check if an equivalent processing can't be done more quickly with a
-            // compiled regexp.
-            // Filter by first letter
-            final int firstCodePoint = text.codePointAt(0);
-            // Filter out words that don't start with a letter or an apostrophe
-            if (!isLetterCheckableByLanguage(firstCodePoint, script)
-                    && '\'' != firstCodePoint) return true;
-
-            // Filter contents
-            final int length = text.length();
-            int letterCount = 0;
-            for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
-                final int codePoint = text.codePointAt(i);
-                // Any word containing a '@' is probably an e-mail address
-                // Any word containing a '/' is probably either an ad-hoc combination of two
-                // words or a URI - in either case we don't want to spell check that
-                if ('@' == codePoint || '/' == codePoint) return true;
-                if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
-            }
-            // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
-            // in this word are letters
-            return (letterCount * 4 < length * 3);
-        }
-
-        private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(
-                TextInfo ti, SentenceSuggestionsInfo ssi) {
-            final String typedText = ti.getText();
-            if (!typedText.contains(SINGLE_QUOTE)) {
-                return null;
-            }
-            final int N = ssi.getSuggestionsCount();
-            final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>();
-            final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
-            final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
-                    new ArrayList<SuggestionsInfo>();
-            for (int i = 0; i < N; ++i) {
-                final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
-                final int flags = si.getSuggestionsAttributes();
-                if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
-                    continue;
-                }
-                final int offset = ssi.getOffsetAt(i);
-                final int length = ssi.getLengthAt(i);
-                final String subText = typedText.substring(offset, offset + length);
-                if (!subText.contains(SINGLE_QUOTE)) {
-                    continue;
-                }
-                final String[] splitTexts = subText.split(SINGLE_QUOTE, -1);
-                if (splitTexts == null || splitTexts.length <= 1) {
-                    continue;
-                }
-                final int splitNum = splitTexts.length;
-                for (int j = 0; j < splitNum; ++j) {
-                    final String splitText = splitTexts[j];
-                    if (TextUtils.isEmpty(splitText)) {
-                        continue;
-                    }
-                    if (mSuggestionsCache.getSuggestionsFromCache(splitText) == null) {
-                        continue;
-                    }
-                    final int newLength = splitText.length();
-                    // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
-                    final int newFlags = 0;
-                    final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
-                    newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
-                    if (DBG) {
-                        Log.d(TAG, "Override and remove old span over: "
-                                + splitText + ", " + offset + "," + newLength);
-                    }
-                    additionalOffsets.add(offset);
-                    additionalLengths.add(newLength);
-                    additionalSuggestionsInfos.add(newSi);
-                }
-            }
-            final int additionalSize = additionalOffsets.size();
-            if (additionalSize <= 0) {
-                return null;
-            }
-            final int suggestionsSize = N + additionalSize;
-            final int[] newOffsets = new int[suggestionsSize];
-            final int[] newLengths = new int[suggestionsSize];
-            final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
-            int i;
-            for (i = 0; i < N; ++i) {
-                newOffsets[i] = ssi.getOffsetAt(i);
-                newLengths[i] = ssi.getLengthAt(i);
-                newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
-            }
-            for (; i < suggestionsSize; ++i) {
-                newOffsets[i] = additionalOffsets.get(i - N);
-                newLengths[i] = additionalLengths.get(i - N);
-                newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
-            }
-            return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
-        }
-
-        @Override
-        public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(
-                TextInfo[] textInfos, int suggestionsLimit) {
-            final SentenceSuggestionsInfo[] retval = super.onGetSentenceSuggestionsMultiple(
-                    textInfos, suggestionsLimit);
-            if (retval == null || retval.length != textInfos.length) {
-                return retval;
-            }
-            for (int i = 0; i < retval.length; ++i) {
-                final SentenceSuggestionsInfo tempSsi =
-                        fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
-                if (tempSsi != null) {
-                    retval[i] = tempSsi;
-                }
-            }
-            return retval;
-        }
-
-        @Override
-        public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
-                int suggestionsLimit, boolean sequentialWords) {
-            final int length = textInfos.length;
-            final SuggestionsInfo[] retval = new SuggestionsInfo[length];
-            for (int i = 0; i < length; ++i) {
-                final String prevWord;
-                if (sequentialWords && i > 0) {
-                    final String prevWordCandidate = textInfos[i - 1].getText();
-                    // Note that an empty string would be used to indicate the initial word
-                    // in the future.
-                    prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
-                } else {
-                    prevWord = null;
-                }
-                retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
-                retval[i].setCookieAndSequence(
-                        textInfos[i].getCookie(), textInfos[i].getSequence());
-            }
-            return retval;
-        }
-
-        // Note : this must be reentrant
-        /**
-         * Gets a list of suggestions for a specific string. This returns a list of possible
-         * corrections for the text passed as an argument. It may split or group words, and
-         * even perform grammatical analysis.
-         */
-        @Override
-        public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
-                final int suggestionsLimit) {
-            return onGetSuggestions(textInfo, null, suggestionsLimit);
-        }
-
-        private SuggestionsInfo onGetSuggestions(
-                final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
-            try {
-                final String inText = textInfo.getText();
-                final SuggestionsParams cachedSuggestionsParams =
-                        mSuggestionsCache.getSuggestionsFromCache(inText);
-                if (cachedSuggestionsParams != null) {
-                    if (DBG) {
-                        Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
-                    }
-                    return new SuggestionsInfo(
-                            cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
-                }
-
-                if (shouldFilterOut(inText, mScript)) {
-                    DictAndProximity dictInfo = null;
-                    try {
-                        dictInfo = mDictionaryPool.takeOrGetNull();
-                        if (null == dictInfo) return getNotInDictEmptySuggestions();
-                        return dictInfo.mDictionary.isValidWord(inText) ?
-                                getInDictEmptySuggestions() : getNotInDictEmptySuggestions();
-                    } finally {
-                        if (null != dictInfo) {
-                            if (!mDictionaryPool.offer(dictInfo)) {
-                                Log.e(TAG, "Can't re-insert a dictionary into its pool");
-                            }
-                        }
-                    }
-                }
-                final String text = inText.replaceAll(APOSTROPHE, SINGLE_QUOTE);
-
-                // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
-                final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
-                        mService.mSuggestionThreshold, mService.mRecommendedThreshold,
-                        suggestionsLimit);
-                final WordComposer composer = new WordComposer();
-                final int length = text.length();
-                for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
-                    final int codePoint = text.codePointAt(i);
-                    // The getXYForCodePointAndScript method returns (Y << 16) + X
-                    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, null);
-                    } else {
-                        composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
-                    }
-                }
-
-                final int capitalizeType = getCapitalizationType(text);
-                boolean isInDict = true;
-                DictAndProximity dictInfo = null;
-                try {
-                    dictInfo = mDictionaryPool.takeOrGetNull();
-                    if (null == dictInfo) return getNotInDictEmptySuggestions();
-                    dictInfo.mDictionary.getWords(composer, prevWord, suggestionsGatherer,
-                            dictInfo.mProximityInfo);
-                    isInDict = dictInfo.mDictionary.isValidWord(text);
-                    if (!isInDict && CAPITALIZE_NONE != capitalizeType) {
-                        // We want to test the word again if it's all caps or first caps only.
-                        // If it's fully down, we already tested it, if it's mixed case, we don't
-                        // want to test a lowercase version of it.
-                        isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
-                    }
-                } finally {
-                    if (null != dictInfo) {
-                        if (!mDictionaryPool.offer(dictInfo)) {
-                            Log.e(TAG, "Can't re-insert a dictionary into its pool");
-                        }
-                    }
-                }
-
-                final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
-                        capitalizeType, mLocale);
-
-                if (DBG) {
-                    Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
-                            + suggestionsLimit);
-                    Log.i(TAG, "IsInDict = " + isInDict);
-                    Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
-                    Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
-                    if (null != result.mSuggestions) {
-                        for (String suggestion : result.mSuggestions) {
-                            Log.i(TAG, suggestion);
-                        }
-                    }
-                }
-
-                final int flags =
-                        (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
-                                : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
-                        | (result.mHasRecommendedSuggestions
-                                ? SuggestionsInfoCompatUtils
-                                        .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
-                                : 0);
-                final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
-                mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags);
-                return retval;
-            } catch (RuntimeException e) {
-                // Don't kill the keyboard if there is a bug in the spell checker
-                if (DBG) {
-                    throw e;
-                } else {
-                    Log.e(TAG, "Exception while spellcheking: " + e);
-                    return getNotInDictEmptySuggestions();
-                }
-            }
-        }
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
new file mode 100644
index 0000000..5a1bd37
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -0,0 +1,154 @@
+/*
+ * 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.spellcheck;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.textservice.SentenceSuggestionsInfo;
+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 {
+    private static final String TAG = AndroidSpellCheckerSession.class.getSimpleName();
+    private static final boolean DBG = false;
+    private final static String[] EMPTY_STRING_ARRAY = new String[0];
+
+    public AndroidSpellCheckerSession(AndroidSpellCheckerService service) {
+        super(service);
+    }
+
+    private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti,
+            SentenceSuggestionsInfo ssi) {
+        final String typedText = ti.getText();
+        if (!typedText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
+            return null;
+        }
+        final int N = ssi.getSuggestionsCount();
+        final ArrayList<Integer> additionalOffsets = CollectionUtils.newArrayList();
+        final ArrayList<Integer> additionalLengths = CollectionUtils.newArrayList();
+        final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
+                CollectionUtils.newArrayList();
+        String currentWord = null;
+        for (int i = 0; i < N; ++i) {
+            final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
+            final int flags = si.getSuggestionsAttributes();
+            if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
+                continue;
+            }
+            final int offset = ssi.getOffsetAt(i);
+            final int length = ssi.getLengthAt(i);
+            final String subText = typedText.substring(offset, offset + length);
+            final String prevWord = currentWord;
+            currentWord = subText;
+            if (!subText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
+                continue;
+            }
+            final String[] splitTexts =
+                    subText.split(AndroidSpellCheckerService.SINGLE_QUOTE, -1);
+            if (splitTexts == null || splitTexts.length <= 1) {
+                continue;
+            }
+            final int splitNum = splitTexts.length;
+            for (int j = 0; j < splitNum; ++j) {
+                final String splitText = splitTexts[j];
+                if (TextUtils.isEmpty(splitText)) {
+                    continue;
+                }
+                if (mSuggestionsCache.getSuggestionsFromCache(splitText, prevWord) == null) {
+                    continue;
+                }
+                final int newLength = splitText.length();
+                // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
+                final int newFlags = 0;
+                final SuggestionsInfo newSi =
+                        new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
+                newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
+                if (DBG) {
+                    Log.d(TAG, "Override and remove old span over: " + splitText + ", "
+                            + offset + "," + newLength);
+                }
+                additionalOffsets.add(offset);
+                additionalLengths.add(newLength);
+                additionalSuggestionsInfos.add(newSi);
+            }
+        }
+        final int additionalSize = additionalOffsets.size();
+        if (additionalSize <= 0) {
+            return null;
+        }
+        final int suggestionsSize = N + additionalSize;
+        final int[] newOffsets = new int[suggestionsSize];
+        final int[] newLengths = new int[suggestionsSize];
+        final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
+        int i;
+        for (i = 0; i < N; ++i) {
+            newOffsets[i] = ssi.getOffsetAt(i);
+            newLengths[i] = ssi.getLengthAt(i);
+            newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
+        }
+        for (; i < suggestionsSize; ++i) {
+            newOffsets[i] = additionalOffsets.get(i - N);
+            newLengths[i] = additionalLengths.get(i - N);
+            newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
+        }
+        return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
+    }
+
+    @Override
+    public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
+            int suggestionsLimit) {
+        final SentenceSuggestionsInfo[] retval =
+                super.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit);
+        if (retval == null || retval.length != textInfos.length) {
+            return retval;
+        }
+        for (int i = 0; i < retval.length; ++i) {
+            final SentenceSuggestionsInfo tempSsi =
+                    fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
+            if (tempSsi != null) {
+                retval[i] = tempSsi;
+            }
+        }
+        return retval;
+    }
+
+    @Override
+    public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
+            int suggestionsLimit, boolean sequentialWords) {
+        final int length = textInfos.length;
+        final SuggestionsInfo[] retval = new SuggestionsInfo[length];
+        for (int i = 0; i < length; ++i) {
+            final String prevWord;
+            if (sequentialWords && i > 0) {
+                final String prevWordCandidate = textInfos[i - 1].getText();
+                // Note that an empty string would be used to indicate the initial word
+                // in the future.
+                prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
+            } else {
+                prevWord = null;
+            }
+            retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
+            retval[i].setCookieAndSequence(textInfos[i].getCookie(),
+                    textInfos[i].getSequence());
+        }
+        return retval;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java
new file mode 100644
index 0000000..8eb1eb6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.spellcheck;
+
+import android.service.textservice.SpellCheckerService.Session;
+
+public abstract class AndroidSpellCheckerSessionFactory {
+    public static Session newInstance(AndroidSpellCheckerService service) {
+        return new AndroidSpellCheckerSession(service);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
new file mode 100644
index 0000000..d9b622a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -0,0 +1,326 @@
+/*
+ * 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.spellcheck;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.provider.UserDictionary.Words;
+import android.service.textservice.SpellCheckerService.Session;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.textservice.SuggestionsInfo;
+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;
+import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+public abstract class AndroidWordLevelSpellCheckerSession extends Session {
+    private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    // Immutable, but need the locale which is not available in the constructor yet
+    private DictionaryPool mDictionaryPool;
+    // Likewise
+    private Locale mLocale;
+    // Cache this for performance
+    private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
+    private final AndroidSpellCheckerService mService;
+    protected final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
+    private final ContentObserver mObserver;
+
+    private static class SuggestionsParams {
+        public final String[] mSuggestions;
+        public final int mFlags;
+        public SuggestionsParams(String[] suggestions, int flags) {
+            mSuggestions = suggestions;
+            mFlags = flags;
+        }
+    }
+
+    protected static class SuggestionsCache {
+        private static final char CHAR_DELIMITER = '\uFFFC';
+        private static final int MAX_CACHE_SIZE = 50;
+        private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
+                new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
+
+        // TODO: Support n-gram input
+        private static String generateKey(String query, String prevWord) {
+            if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) {
+                return query;
+            }
+            return query + CHAR_DELIMITER + prevWord;
+        }
+
+        // TODO: Support n-gram input
+        public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) {
+            return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord));
+        }
+
+        // TODO: Support n-gram input
+        public void putSuggestionsToCache(
+                String query, String prevWord, String[] suggestions, int flags) {
+            if (suggestions == null || TextUtils.isEmpty(query)) {
+                return;
+            }
+            mUnigramSuggestionsInfoCache.put(
+                    generateKey(query, prevWord), new SuggestionsParams(suggestions, flags));
+        }
+
+        public void clearCache() {
+            mUnigramSuggestionsInfoCache.evictAll();
+        }
+    }
+
+    AndroidWordLevelSpellCheckerSession(final AndroidSpellCheckerService service) {
+        mService = service;
+        final ContentResolver cres = service.getContentResolver();
+
+        mObserver = new ContentObserver(null) {
+            @Override
+            public void onChange(boolean self) {
+                mSuggestionsCache.clearCache();
+            }
+        };
+        cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
+    }
+
+    @Override
+    public void onCreate() {
+        final String localeString = getLocale();
+        mDictionaryPool = mService.getDictionaryPool(localeString);
+        mLocale = LocaleUtils.constructLocaleFromString(localeString);
+        mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale);
+    }
+
+    @Override
+    public void onClose() {
+        final ContentResolver cres = mService.getContentResolver();
+        cres.unregisterContentObserver(mObserver);
+    }
+
+    /*
+     * Returns whether the code point is a letter that makes sense for the specified
+     * locale for this spell checker.
+     * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
+     * and is limited to EFIGS languages and Russian.
+     * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
+     * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
+     */
+    private static boolean isLetterCheckableByLanguage(final int codePoint,
+            final int script) {
+        switch (script) {
+        case AndroidSpellCheckerService.SCRIPT_LATIN:
+            // Our supported latin script dictionaries (EFIGS) at the moment only include
+            // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
+            // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
+            // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
+            // excluded from isLetter anyway.
+            return codePoint <= 0x2AF && Character.isLetter(codePoint);
+        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+            // All Cyrillic characters are in the 400~52F block. There are some in the upper
+            // Unicode range, but they are archaic characters that are not used in modern
+            // russian and are not used by our dictionary.
+            return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
+        default:
+            // Should never come here
+            throw new RuntimeException("Impossible value of script: " + script);
+        }
+    }
+
+    /**
+     * Finds out whether a particular string should be filtered out of spell checking.
+     *
+     * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
+     * we know we will never recognize, this accepts a script identifier that should be one
+     * of the SCRIPT_* constants defined above, to rule out quickly characters from very
+     * different languages.
+     *
+     * @param text the string to evaluate.
+     * @param script the identifier for the script this spell checker recognizes
+     * @return true if we should filter this text out, false otherwise
+     */
+    private static boolean shouldFilterOut(final String text, final int script) {
+        if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
+
+        // TODO: check if an equivalent processing can't be done more quickly with a
+        // compiled regexp.
+        // Filter by first letter
+        final int firstCodePoint = text.codePointAt(0);
+        // Filter out words that don't start with a letter or an apostrophe
+        if (!isLetterCheckableByLanguage(firstCodePoint, script)
+                && '\'' != firstCodePoint) return true;
+
+        // Filter contents
+        final int length = text.length();
+        int letterCount = 0;
+        for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+            final int codePoint = text.codePointAt(i);
+            // Any word containing a '@' is probably an e-mail address
+            // Any word containing a '/' is probably either an ad-hoc combination of two
+            // words or a URI - in either case we don't want to spell check that
+            if ('@' == codePoint || '/' == codePoint) return true;
+            if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
+        }
+        // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
+        // in this word are letters
+        return (letterCount * 4 < length * 3);
+    }
+
+    // Note : this must be reentrant
+    /**
+     * Gets a list of suggestions for a specific string. This returns a list of possible
+     * corrections for the text passed as an argument. It may split or group words, and
+     * even perform grammatical analysis.
+     */
+    @Override
+    public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
+            final int suggestionsLimit) {
+        return onGetSuggestions(textInfo, null, suggestionsLimit);
+    }
+
+    protected SuggestionsInfo onGetSuggestions(
+            final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
+        try {
+            final String inText = textInfo.getText();
+            final SuggestionsParams cachedSuggestionsParams =
+                    mSuggestionsCache.getSuggestionsFromCache(inText, prevWord);
+            if (cachedSuggestionsParams != null) {
+                if (DBG) {
+                    Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
+                }
+                return new SuggestionsInfo(
+                        cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
+            }
+
+            if (shouldFilterOut(inText, mScript)) {
+                DictAndProximity dictInfo = null;
+                try {
+                    dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+                    if (!DictionaryPool.isAValidDictionary(dictInfo)) {
+                        return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+                    }
+                    return dictInfo.mDictionary.isValidWord(inText)
+                            ? AndroidSpellCheckerService.getInDictEmptySuggestions()
+                            : AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+                } finally {
+                    if (null != dictInfo) {
+                        if (!mDictionaryPool.offer(dictInfo)) {
+                            Log.e(TAG, "Can't re-insert a dictionary into its pool");
+                        }
+                    }
+                }
+            }
+            final String text = inText.replaceAll(
+                    AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE);
+
+            // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
+            //final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
+            //mService.mSuggestionThreshold, mService.mRecommendedThreshold,
+            //suggestionsLimit);
+            final SuggestionsGatherer suggestionsGatherer = mService.newSuggestionsGatherer(
+                    text, suggestionsLimit);
+            final WordComposer composer = new WordComposer();
+            final int length = text.length();
+            for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+                final int codePoint = text.codePointAt(i);
+                // The getXYForCodePointAndScript method returns (Y << 16) + X
+                final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
+                        codePoint, mScript);
+                if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
+                    composer.add(codePoint,
+                            Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+                } else {
+                    composer.add(codePoint, xy & 0xFFFF, xy >> 16);
+                }
+            }
+
+            final int capitalizeType = AndroidSpellCheckerService.getCapitalizationType(text);
+            boolean isInDict = true;
+            DictAndProximity dictInfo = null;
+            try {
+                dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+                if (!DictionaryPool.isAValidDictionary(dictInfo)) {
+                    return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+                }
+                final ArrayList<SuggestedWordInfo> suggestions =
+                        dictInfo.mDictionary.getSuggestions(composer, prevWord,
+                                dictInfo.mProximityInfo);
+                for (final SuggestedWordInfo suggestion : suggestions) {
+                    final String suggestionStr = suggestion.mWord.toString();
+                    suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
+                            suggestionStr.length(), suggestion.mScore);
+                }
+                isInDict = dictInfo.mDictionary.isValidWord(text);
+                if (!isInDict && AndroidSpellCheckerService.CAPITALIZE_NONE != capitalizeType) {
+                    // We want to test the word again if it's all caps or first caps only.
+                    // If it's fully down, we already tested it, if it's mixed case, we don't
+                    // want to test a lowercase version of it.
+                    isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
+                }
+            } finally {
+                if (null != dictInfo) {
+                    if (!mDictionaryPool.offer(dictInfo)) {
+                        Log.e(TAG, "Can't re-insert a dictionary into its pool");
+                    }
+                }
+            }
+
+            final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
+                    capitalizeType, mLocale);
+
+            if (DBG) {
+                Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
+                        + suggestionsLimit);
+                Log.i(TAG, "IsInDict = " + isInDict);
+                Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
+                Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
+                if (null != result.mSuggestions) {
+                    for (String suggestion : result.mSuggestions) {
+                        Log.i(TAG, suggestion);
+                    }
+                }
+            }
+
+            final int flags =
+                    (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
+                            : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
+                    | (result.mHasRecommendedSuggestions
+                            ? SuggestionsInfoCompatUtils
+                                    .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
+                            : 0);
+            final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
+            mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags);
+            return retval;
+        } catch (RuntimeException e) {
+            // Don't kill the keyboard if there is a bug in the spell checker
+            if (DBG) {
+                throw e;
+            } else {
+                Log.e(TAG, "Exception while spellcheking: " + e);
+                return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+            }
+        }
+    }
+}
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/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index c6fe43b..1f883aa 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -23,7 +23,9 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.keyboard.internal.KeyboardParams;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.Utils;
@@ -31,145 +33,149 @@
 public class MoreSuggestions extends Keyboard {
     public static final int SUGGESTION_CODE_BASE = 1024;
 
-    MoreSuggestions(Builder.MoreSuggestionsParam params) {
+    MoreSuggestions(final MoreSuggestionsParam params) {
         super(params);
     }
 
-    public static class Builder extends Keyboard.Builder<Builder.MoreSuggestionsParam> {
+    private static class MoreSuggestionsParam extends KeyboardParams {
+        private final int[] mWidths = new int[SuggestionStripView.MAX_SUGGESTIONS];
+        private final int[] mRowNumbers = new int[SuggestionStripView.MAX_SUGGESTIONS];
+        private final int[] mColumnOrders = new int[SuggestionStripView.MAX_SUGGESTIONS];
+        private final int[] mNumColumnsInRow = new int[SuggestionStripView.MAX_SUGGESTIONS];
+        private static final int MAX_COLUMNS_IN_ROW = 3;
+        private int mNumRows;
+        public Drawable mDivider;
+        public int mDividerWidth;
+
+        public MoreSuggestionsParam() {
+            super();
+        }
+
+        public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth,
+                final int minWidth, final int maxRow, final MoreSuggestionsView view) {
+            clearKeys();
+            final Resources res = view.getContext().getResources();
+            mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
+            mDividerWidth = mDivider.getIntrinsicWidth();
+            final int padding = (int) res.getDimension(
+                    R.dimen.more_suggestions_key_horizontal_padding);
+            final Paint paint = view.newDefaultLabelPaint();
+
+            int row = 0;
+            int pos = fromPos, rowStartPos = fromPos;
+            final int size = Math.min(suggestions.size(), SuggestionStripView.MAX_SUGGESTIONS);
+            while (pos < size) {
+                final String word = suggestions.getWord(pos).toString();
+                // TODO: Should take care of text x-scaling.
+                mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding;
+                final int numColumn = pos - rowStartPos + 1;
+                final int columnWidth =
+                        (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
+                if (numColumn > MAX_COLUMNS_IN_ROW
+                        || !fitInWidth(rowStartPos, pos + 1, columnWidth)) {
+                    if ((row + 1) >= maxRow) {
+                        break;
+                    }
+                    mNumColumnsInRow[row] = pos - rowStartPos;
+                    rowStartPos = pos;
+                    row++;
+                }
+                mColumnOrders[pos] = pos - rowStartPos;
+                mRowNumbers[pos] = row;
+                pos++;
+            }
+            mNumColumnsInRow[row] = pos - rowStartPos;
+            mNumRows = row + 1;
+            mBaseWidth = mOccupiedWidth = Math.max(
+                    minWidth, calcurateMaxRowWidth(fromPos, pos));
+            mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
+            return pos - fromPos;
+        }
+
+        private boolean fitInWidth(final int startPos, final int endPos, final int width) {
+            for (int pos = startPos; pos < endPos; pos++) {
+                if (mWidths[pos] > width)
+                    return false;
+            }
+            return true;
+        }
+
+        private int calcurateMaxRowWidth(final int startPos, final int endPos) {
+            int maxRowWidth = 0;
+            int pos = startPos;
+            for (int row = 0; row < mNumRows; row++) {
+                final int numColumnInRow = mNumColumnsInRow[row];
+                int maxKeyWidth = 0;
+                while (pos < endPos && mRowNumbers[pos] == row) {
+                    maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]);
+                    pos++;
+                }
+                maxRowWidth = Math.max(maxRowWidth,
+                        maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1));
+            }
+            return maxRowWidth;
+        }
+
+        private static final int[][] COLUMN_ORDER_TO_NUMBER = {
+            { 0, },
+            { 1, 0, },
+            { 2, 0, 1},
+        };
+
+        public int getNumColumnInRow(final int pos) {
+            return mNumColumnsInRow[mRowNumbers[pos]];
+        }
+
+        public int getColumnNumber(final int pos) {
+            final int columnOrder = mColumnOrders[pos];
+            final int numColumn = getNumColumnInRow(pos);
+            return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder];
+        }
+
+        public int getX(final int pos) {
+            final int columnNumber = getColumnNumber(pos);
+            return columnNumber * (getWidth(pos) + mDividerWidth);
+        }
+
+        public int getY(final int pos) {
+            final int row = mRowNumbers[pos];
+            return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
+        }
+
+        public int getWidth(final int pos) {
+            final int numColumnInRow = getNumColumnInRow(pos);
+            return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow;
+        }
+
+        public void markAsEdgeKey(final Key key, final int pos) {
+            final int row = mRowNumbers[pos];
+            if (row == 0)
+                key.markAsBottomEdge(this);
+            if (row == mNumRows - 1)
+                key.markAsTopEdge(this);
+
+            final int numColumnInRow = mNumColumnsInRow[row];
+            final int column = getColumnNumber(pos);
+            if (column == 0)
+                key.markAsLeftEdge(this);
+            if (column == numColumnInRow - 1)
+                key.markAsRightEdge(this);
+        }
+    }
+
+    public static class Builder extends KeyboardBuilder<MoreSuggestionsParam> {
         private final MoreSuggestionsView mPaneView;
         private SuggestedWords mSuggestions;
         private int mFromPos;
         private int mToPos;
 
-        public static class MoreSuggestionsParam extends Keyboard.Params {
-            private final int[] mWidths = new int[SuggestionsView.MAX_SUGGESTIONS];
-            private final int[] mRowNumbers = new int[SuggestionsView.MAX_SUGGESTIONS];
-            private final int[] mColumnOrders = new int[SuggestionsView.MAX_SUGGESTIONS];
-            private final int[] mNumColumnsInRow = new int[SuggestionsView.MAX_SUGGESTIONS];
-            private static final int MAX_COLUMNS_IN_ROW = 3;
-            private int mNumRows;
-            public Drawable mDivider;
-            public int mDividerWidth;
-
-            public int layout(SuggestedWords suggestions, int fromPos, int maxWidth, int minWidth,
-                    int maxRow, MoreSuggestionsView view) {
-                clearKeys();
-                final Resources res = view.getContext().getResources();
-                mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
-                mDividerWidth = mDivider.getIntrinsicWidth();
-                final int padding = (int) res.getDimension(
-                        R.dimen.more_suggestions_key_horizontal_padding);
-                final Paint paint = view.newDefaultLabelPaint();
-
-                int row = 0;
-                int pos = fromPos, rowStartPos = fromPos;
-                final int size = Math.min(suggestions.size(), SuggestionsView.MAX_SUGGESTIONS);
-                while (pos < size) {
-                    final String word = suggestions.getWord(pos).toString();
-                    // TODO: Should take care of text x-scaling.
-                    mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding;
-                    final int numColumn = pos - rowStartPos + 1;
-                    final int columnWidth =
-                            (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
-                    if (numColumn > MAX_COLUMNS_IN_ROW
-                            || !fitInWidth(rowStartPos, pos + 1, columnWidth)) {
-                        if ((row + 1) >= maxRow) {
-                            break;
-                        }
-                        mNumColumnsInRow[row] = pos - rowStartPos;
-                        rowStartPos = pos;
-                        row++;
-                    }
-                    mColumnOrders[pos] = pos - rowStartPos;
-                    mRowNumbers[pos] = row;
-                    pos++;
-                }
-                mNumColumnsInRow[row] = pos - rowStartPos;
-                mNumRows = row + 1;
-                mBaseWidth = mOccupiedWidth = Math.max(
-                        minWidth, calcurateMaxRowWidth(fromPos, pos));
-                mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
-                return pos - fromPos;
-            }
-
-            private boolean fitInWidth(int startPos, int endPos, int width) {
-                for (int pos = startPos; pos < endPos; pos++) {
-                    if (mWidths[pos] > width)
-                        return false;
-                }
-                return true;
-            }
-
-            private int calcurateMaxRowWidth(int startPos, int endPos) {
-                int maxRowWidth = 0;
-                int pos = startPos;
-                for (int row = 0; row < mNumRows; row++) {
-                    final int numColumnInRow = mNumColumnsInRow[row];
-                    int maxKeyWidth = 0;
-                    while (pos < endPos && mRowNumbers[pos] == row) {
-                        maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]);
-                        pos++;
-                    }
-                    maxRowWidth = Math.max(maxRowWidth,
-                            maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1));
-                }
-                return maxRowWidth;
-            }
-
-            private static final int[][] COLUMN_ORDER_TO_NUMBER = {
-                { 0, },
-                { 1, 0, },
-                { 2, 0, 1},
-            };
-
-            public int getNumColumnInRow(int pos) {
-                return mNumColumnsInRow[mRowNumbers[pos]];
-            }
-
-            public int getColumnNumber(int pos) {
-                final int columnOrder = mColumnOrders[pos];
-                final int numColumn = getNumColumnInRow(pos);
-                return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder];
-            }
-
-            public int getX(int pos) {
-                final int columnNumber = getColumnNumber(pos);
-                return columnNumber * (getWidth(pos) + mDividerWidth);
-            }
-
-            public int getY(int pos) {
-                final int row = mRowNumbers[pos];
-                return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
-            }
-
-            public int getWidth(int pos) {
-                final int numColumnInRow = getNumColumnInRow(pos);
-                return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow;
-            }
-
-            public void markAsEdgeKey(Key key, int pos) {
-                final int row = mRowNumbers[pos];
-                if (row == 0)
-                    key.markAsBottomEdge(this);
-                if (row == mNumRows - 1)
-                    key.markAsTopEdge(this);
-
-                final int numColumnInRow = mNumColumnsInRow[row];
-                final int column = getColumnNumber(pos);
-                if (column == 0)
-                    key.markAsLeftEdge(this);
-                if (column == numColumnInRow - 1)
-                    key.markAsRightEdge(this);
-            }
-        }
-
-        public Builder(MoreSuggestionsView paneView) {
+        public Builder(final MoreSuggestionsView paneView) {
             super(paneView.getContext(), new MoreSuggestionsParam());
             mPaneView = paneView;
         }
 
-        public Builder layout(SuggestedWords suggestions, int fromPos, int maxWidth,
-                int minWidth, int maxRow) {
+        public Builder layout(final SuggestedWords suggestions, final int fromPos,
+                final int maxWidth, final int minWidth, final int maxRow) {
             final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
             final int xmlId = R.xml.kbd_suggestions_pane_template;
             load(xmlId, keyboard.mId);
@@ -183,25 +189,6 @@
             return this;
         }
 
-        private static class Divider extends Key.Spacer {
-            private final Drawable mIcon;
-
-            public Divider(Keyboard.Params params, Drawable icon, int x, int y, int width,
-                    int height) {
-                super(params, x, y, width, height);
-                mIcon = icon;
-            }
-
-            @Override
-            public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
-                // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
-                // constructor.
-                // TODO: Drawable itself should have an alpha value.
-                mIcon.setAlpha(128);
-                return mIcon;
-            }
-        }
-
         @Override
         public MoreSuggestions build() {
             final MoreSuggestionsParam params = mParams;
@@ -228,4 +215,23 @@
             return new MoreSuggestions(params);
         }
     }
+
+    private static class Divider extends Key.Spacer {
+        private final Drawable mIcon;
+
+        public Divider(final KeyboardParams params, final Drawable icon, final int x,
+                final int y, final int width, final int height) {
+            super(params, x, y, width, height);
+            mIcon = icon;
+        }
+
+        @Override
+        public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
+            // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
+            // constructor.
+            // TODO: Drawable itself should have an alpha value.
+            mIcon.setAlpha(128);
+            return mIcon;
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index 19287e3..5b23d7f 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -68,7 +68,7 @@
         @Override
         public void onCodeInput(int primaryCode, int x, int y) {
             final int index = primaryCode - MoreSuggestions.SUGGESTION_CODE_BASE;
-            if (index >= 0 && index < SuggestionsView.MAX_SUGGESTIONS) {
+            if (index >= 0 && index < SuggestionStripView.MAX_SUGGESTIONS) {
                 mListener.onCustomRequest(index);
             }
         }
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
similarity index 88%
rename from java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java
rename to java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index e86390b..9e8ab81 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -57,22 +57,24 @@
 import com.android.inputmethod.keyboard.MoreKeysPanel;
 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.ResearchLogger;
+import com.android.inputmethod.latin.ResourceUtils;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
-import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
 
 import java.util.ArrayList;
 
-public class SuggestionsView extends RelativeLayout implements OnClickListener,
+public class SuggestionStripView extends RelativeLayout implements OnClickListener,
         OnLongClickListener {
     public interface Listener {
-        public boolean addWordToDictionary(String word);
-        public void pickSuggestionManually(int index, CharSequence word, int x, int y);
+        public boolean addWordToUserDictionary(String word);
+        public void pickSuggestionManually(int index, CharSequence word);
     }
 
     // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
@@ -88,9 +90,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;
@@ -98,24 +100,24 @@
     private Listener mListener;
     private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
 
-    private final SuggestionsViewParams mParams;
+    private final SuggestionStripViewParams mParams;
     private static final float MIN_TEXT_XSCALE = 0.70f;
 
     private final UiHandler mHandler = new UiHandler(this);
 
-    private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionsView> {
+    private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionStripView> {
         private static final int MSG_HIDE_PREVIEW = 0;
 
-        public UiHandler(SuggestionsView outerInstance) {
+        public UiHandler(SuggestionStripView outerInstance) {
             super(outerInstance);
         }
 
         @Override
         public void dispatchMessage(Message msg) {
-            final SuggestionsView suggestionsView = getOuterInstance();
+            final SuggestionStripView suggestionStripView = getOuterInstance();
             switch (msg.what) {
             case MSG_HIDE_PREVIEW:
-                suggestionsView.hidePreview();
+                suggestionStripView.hidePreview();
                 break;
             }
         }
@@ -129,9 +131,9 @@
         }
     }
 
-    private static class SuggestionsViewParams {
+    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 +169,7 @@
 
         private final int mSuggestionStripOption;
 
-        private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
+        private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList();
 
         public boolean mMoreSuggestionsAvailable;
 
@@ -175,7 +177,7 @@
         private final TextView mLeftwardsArrowView;
         private final TextView mHintToSaveView;
 
-        public SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle,
+        public SuggestionStripViewParams(Context context, AttributeSet attrs, int defStyle,
                 ArrayList<TextView> words, ArrayList<View> dividers, ArrayList<TextView> infos) {
             mWords = words;
             mDividers = dividers;
@@ -191,38 +193,39 @@
             final Resources res = word.getResources();
             mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
 
-            final TypedArray a = context.obtainStyledAttributes(
-                    attrs, R.styleable.SuggestionsView, defStyle, R.style.SuggestionsViewStyle);
-            mSuggestionStripOption = a.getInt(R.styleable.SuggestionsView_suggestionStripOption, 0);
-            final float alphaValidTypedWord = getPercent(a,
-                    R.styleable.SuggestionsView_alphaValidTypedWord, 100);
-            final float alphaTypedWord = getPercent(a,
-                    R.styleable.SuggestionsView_alphaTypedWord, 100);
-            final float alphaAutoCorrect = getPercent(a,
-                    R.styleable.SuggestionsView_alphaAutoCorrect, 100);
-            final float alphaSuggested = getPercent(a,
-                    R.styleable.SuggestionsView_alphaSuggested, 100);
-            mAlphaObsoleted = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100);
-            mColorValidTypedWord = applyAlpha(
-                    a.getColor(R.styleable.SuggestionsView_colorValidTypedWord, 0),
-                    alphaValidTypedWord);
-            mColorTypedWord = applyAlpha(
-                    a.getColor(R.styleable.SuggestionsView_colorTypedWord, 0), alphaTypedWord);
-            mColorAutoCorrect = applyAlpha(
-                    a.getColor(R.styleable.SuggestionsView_colorAutoCorrect, 0), alphaAutoCorrect);
-            mColorSuggested = applyAlpha(
-                    a.getColor(R.styleable.SuggestionsView_colorSuggested, 0), alphaSuggested);
+            final TypedArray a = context.obtainStyledAttributes(attrs,
+                    R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
+            mSuggestionStripOption = a.getInt(
+                    R.styleable.SuggestionStripView_suggestionStripOption, 0);
+            final float alphaValidTypedWord = ResourceUtils.getFraction(a,
+                    R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
+            final float alphaTypedWord = ResourceUtils.getFraction(a,
+                    R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
+            final float alphaAutoCorrect = ResourceUtils.getFraction(a,
+                    R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
+            final float alphaSuggested = ResourceUtils.getFraction(a,
+                    R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
+            mAlphaObsoleted = ResourceUtils.getFraction(a,
+                    R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
+            mColorValidTypedWord = applyAlpha(a.getColor(
+                    R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
+            mColorTypedWord = applyAlpha(a.getColor(
+                    R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
+            mColorAutoCorrect = applyAlpha(a.getColor(
+                    R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
+            mColorSuggested = applyAlpha(a.getColor(
+                    R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
             mSuggestionsCountInStrip = a.getInt(
-                    R.styleable.SuggestionsView_suggestionsCountInStrip,
+                    R.styleable.SuggestionStripView_suggestionsCountInStrip,
                     DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
-            mCenterSuggestionWeight = getPercent(a,
-                    R.styleable.SuggestionsView_centerSuggestionPercentile,
+            mCenterSuggestionWeight = ResourceUtils.getFraction(a,
+                    R.styleable.SuggestionStripView_centerSuggestionPercentile,
                     DEFAULT_CENTER_SUGGESTION_PERCENTILE);
             mMaxMoreSuggestionsRow = a.getInt(
-                    R.styleable.SuggestionsView_maxMoreSuggestionsRow,
+                    R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
                     DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
-            mMinMoreSuggestionsWidth = getRatio(a,
-                    R.styleable.SuggestionsView_minMoreSuggestionsWidth);
+            mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
+                    R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
             a.recycle();
 
             mMoreSuggestionsHint = getMoreSuggestionsHint(res,
@@ -276,16 +279,6 @@
             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;
-        }
-
         private CharSequence getStyledSuggestionWord(SuggestedWords suggestedWords, int pos) {
             final CharSequence word = suggestedWords.getWord(pos);
             final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect();
@@ -336,8 +329,8 @@
             if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
                 // If we auto-correct, then the autocorrection is in slot 0 and the typed word
                 // is in slot 1.
-                if (index == mCenterSuggestionIndex && suggestedWords.mHasAutoCorrectionCandidate
-                        && Suggest.shouldBlockAutoCorrectionBySafetyNet(
+                if (index == mCenterSuggestionIndex
+                        && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
                                 suggestedWords.getWord(1).toString(), suggestedWords.getWord(0))) {
                     return 0xFFFF0000;
                 }
@@ -596,15 +589,15 @@
     }
 
     /**
-     * Construct a {@link SuggestionsView} for showing suggestions to be picked by the user.
+     * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
      * @param context
      * @param attrs
      */
-    public SuggestionsView(Context context, AttributeSet attrs) {
-        this(context, attrs, R.attr.suggestionsViewStyle);
+    public SuggestionStripView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.suggestionStripViewStyle);
     }
 
-    public SuggestionsView(Context context, AttributeSet attrs, int defStyle) {
+    public SuggestionStripView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
 
         final LayoutInflater inflater = LayoutInflater.from(context);
@@ -631,7 +624,8 @@
             mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
         }
 
-        mParams = new SuggestionsViewParams(context, attrs, defStyle, mWords, mDividers, mInfos);
+        mParams = new SuggestionStripViewParams(
+                context, attrs, defStyle, mWords, mDividers, mInfos);
 
         mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
         mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
@@ -677,7 +671,7 @@
         mSuggestedWords = suggestedWords;
         mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth());
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.suggestionsView_setSuggestions(mSuggestedWords);
+            ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
         }
     }
 
@@ -718,19 +712,13 @@
         mPreviewPopup.dismiss();
     }
 
-    private void addToDictionary(CharSequence word) {
-        mListener.addWordToDictionary(word.toString());
-    }
-
     private final KeyboardActionListener mMoreSuggestionsListener =
             new KeyboardActionListener.Adapter() {
         @Override
         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;
         }
@@ -763,7 +751,7 @@
     }
 
     private boolean showMoreSuggestions() {
-        final SuggestionsViewParams params = mParams;
+        final SuggestionStripViewParams params = mParams;
         if (params.mMoreSuggestionsAvailable) {
             final int stripWidth = getWidth();
             final View container = mMoreSuggestionsContainer;
@@ -863,7 +851,7 @@
     @Override
     public void onClick(View view) {
         if (mParams.isAddToDictionaryShowing(view)) {
-            addToDictionary(mParams.getAddToDictionaryWord());
+            mListener.addWordToUserDictionary(mParams.getAddToDictionaryWord().toString());
             clear();
             return;
         }
@@ -876,7 +864,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
new file mode 100644
index 0000000..11eae88
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -0,0 +1,54 @@
+/*
+ * 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.app.Activity;
+import android.os.Bundle;
+import android.widget.CheckBox;
+
+import com.android.inputmethod.latin.R;
+
+public class FeedbackActivity extends Activity {
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        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);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+    }
+
+    @Override
+    public void onBackPressed() {
+        ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+        super.onBackPressed();
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
new file mode 100644
index 0000000..a2e08e2
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -0,0 +1,73 @@
+/*
+ * 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.app.Activity;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.text.Editable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import com.android.inputmethod.latin.R;
+
+public class FeedbackFragment extends Fragment {
+    private EditText mEditText;
+    private CheckBox mCheckBox;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        final View view = inflater.inflate(R.layout.research_feedback_fragment_layout, container,
+                false);
+        mEditText = (EditText) view.findViewById(R.id.research_feedback_contents);
+        mCheckBox = (CheckBox) view.findViewById(R.id.research_feedback_include_history);
+
+        final Button sendButton = (Button) view.findViewById(
+                R.id.research_feedback_send_button);
+        sendButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final Editable editable = mEditText.getText();
+                final String feedbackContents = editable.toString();
+                final boolean includeHistory = mCheckBox.isChecked();
+                ResearchLogger.getInstance().sendFeedback(feedbackContents, includeHistory);
+                final Activity activity = FeedbackFragment.this.getActivity();
+                activity.finish();
+                ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+            }
+        });
+
+        final Button cancelButton = (Button) view.findViewById(
+                R.id.research_feedback_cancel_button);
+        cancelButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final Activity activity = FeedbackFragment.this.getActivity();
+                activity.finish();
+                ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+            }
+        });
+
+        return view;
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackLayout.java b/java/src/com/android/inputmethod/research/FeedbackLayout.java
new file mode 100644
index 0000000..f2cbfe3
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackLayout.java
@@ -0,0 +1,62 @@
+/*
+ * 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.app.Activity;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.LinearLayout;
+
+public class FeedbackLayout extends LinearLayout {
+    private Activity mActivity;
+
+    public FeedbackLayout(Context context) {
+        super(context);
+    }
+
+    public FeedbackLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public FeedbackLayout(Context context, AttributeSet attrs, int defstyle) {
+        super(context, attrs, defstyle);
+    }
+
+    public void setActivity(Activity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public boolean dispatchKeyEventPreIme(KeyEvent event) {
+        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+            KeyEvent.DispatcherState state = getKeyDispatcherState();
+            if (state != null) {
+                if (event.getAction() == KeyEvent.ACTION_DOWN
+                        && event.getRepeatCount() == 0) {
+                    state.startTracking(event, this);
+                    return true;
+                } else if (event.getAction() == KeyEvent.ACTION_UP
+                        && !event.isCanceled() && state.isTracking(event)) {
+                    mActivity.onBackPressed();
+                    return true;
+                }
+            }
+        }
+        return super.dispatchKeyEventPreIme(event);
+    }
+}
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
new file mode 100644
index 0000000..70c38e9
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -0,0 +1,320 @@
+/*
+ * 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.SharedPreferences;
+import android.os.SystemClock;
+import android.util.JsonWriter;
+import android.util.Log;
+import android.view.inputmethod.CompletionInfo;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+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;
+
+/**
+ * Logs the use of the LatinIME keyboard.
+ *
+ * This class logs operations on the IME keyboard, including what the user has typed.
+ * Data is stored locally in a file in app-specific storage.
+ *
+ * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
+ */
+public class ResearchLog {
+    private static final String TAG = ResearchLog.class.getSimpleName();
+    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;
+
+    /* 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 static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
+            new OutputStreamWriter(new NullOutputStream()));
+    private static class NullOutputStream extends OutputStream {
+        /** {@inheritDoc} */
+        @Override
+        public void write(byte[] buffer, int offset, int count) {
+            // nop
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void write(byte[] buffer) {
+            // nop
+        }
+
+        @Override
+        public void write(int oneByte) {
+        }
+    }
+
+    public ResearchLog(final File outputFile) {
+        if (outputFile == null) {
+            throw new IllegalArgumentException();
+        }
+        mExecutor = Executors.newSingleThreadScheduledExecutor();
+        mFile = outputFile;
+    }
+
+    public synchronized void close(final Runnable onClosed) {
+        mExecutor.submit(new Callable<Object>() {
+            @Override
+            public Object call() throws Exception {
+                try {
+                    if (mHasWrittenData) {
+                        mJsonWriter.endArray();
+                        mJsonWriter.flush();
+                        mJsonWriter.close();
+                        if (DEBUG) {
+                            Log.d(TAG, "wrote log to " + mFile);
+                        }
+                        mHasWrittenData = false;
+                    } else {
+                        if (DEBUG) {
+                            Log.d(TAG, "close() called, but no data, not outputting");
+                        }
+                    }
+                } catch (Exception e) {
+                    Log.d(TAG, "error when closing ResearchLog:");
+                    e.printStackTrace();
+                } finally {
+                    if (mFile.exists()) {
+                        mFile.setWritable(false, false);
+                    }
+                    if (onClosed != null) {
+                        onClosed.run();
+                    }
+                }
+                return null;
+            }
+        });
+        removeAnyScheduledFlush();
+        mExecutor.shutdown();
+    }
+
+    private boolean mIsAbortSuccessful;
+
+    public synchronized void abort() {
+        mExecutor.submit(new Callable<Object>() {
+            @Override
+            public Object call() throws Exception {
+                try {
+                    if (mHasWrittenData) {
+                        mJsonWriter.endArray();
+                        mJsonWriter.close();
+                        mHasWrittenData = false;
+                    }
+                } finally {
+                    mIsAbortSuccessful = mFile.delete();
+                }
+                return null;
+            }
+        });
+        removeAnyScheduledFlush();
+        mExecutor.shutdown();
+    }
+
+    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() {
+        removeAnyScheduledFlush();
+        mExecutor.submit(mFlushCallable);
+    }
+
+    private final Callable<Object> mFlushCallable = new Callable<Object>() {
+        @Override
+        public Object call() throws Exception {
+            mJsonWriter.flush();
+            return null;
+        }
+    };
+
+    private ScheduledFuture<Object> mFlushFuture;
+
+    private void removeAnyScheduledFlush() {
+        if (mFlushFuture != null) {
+            mFlushFuture.cancel(false);
+            mFlushFuture = null;
+        }
+    }
+
+    private void scheduleFlush() {
+        removeAnyScheduledFlush();
+        mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
+    }
+
+    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.
+        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.beginArray();
+                mHasWrittenData = true;
+            }
+            mJsonWriter.beginObject();
+            mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
+            mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
+            mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
+            final int length = values.length;
+            for (int i = 0; i < length; i++) {
+                mJsonWriter.name(keys[i + 1]);
+                Object value = values[i];
+                if (value instanceof CharSequence) {
+                    mJsonWriter.value(value.toString());
+                } else if (value instanceof Number) {
+                    mJsonWriter.value((Number) value);
+                } else if (value instanceof Boolean) {
+                    mJsonWriter.value((Boolean) value);
+                } else if (value instanceof CompletionInfo[]) {
+                    CompletionInfo[] ci = (CompletionInfo[]) value;
+                    mJsonWriter.beginArray();
+                    for (int j = 0; j < ci.length; j++) {
+                        mJsonWriter.value(ci[j].toString());
+                    }
+                    mJsonWriter.endArray();
+                } else if (value instanceof SharedPreferences) {
+                    SharedPreferences prefs = (SharedPreferences) value;
+                    mJsonWriter.beginObject();
+                    for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
+                        mJsonWriter.name(entry.getKey());
+                        final Object innerValue = entry.getValue();
+                        if (innerValue == null) {
+                            mJsonWriter.nullValue();
+                        } else if (innerValue instanceof Boolean) {
+                            mJsonWriter.value((Boolean) innerValue);
+                        } else if (innerValue instanceof Number) {
+                            mJsonWriter.value((Number) innerValue);
+                        } else {
+                            mJsonWriter.value(innerValue.toString());
+                        }
+                    }
+                    mJsonWriter.endObject();
+                } else if (value instanceof Key[]) {
+                    Key[] keyboardKeys = (Key[]) value;
+                    mJsonWriter.beginArray();
+                    for (Key keyboardKey : keyboardKeys) {
+                        mJsonWriter.beginObject();
+                        mJsonWriter.name("code").value(keyboardKey.mCode);
+                        mJsonWriter.name("altCode").value(keyboardKey.getAltCode());
+                        mJsonWriter.name("x").value(keyboardKey.mX);
+                        mJsonWriter.name("y").value(keyboardKey.mY);
+                        mJsonWriter.name("w").value(keyboardKey.mWidth);
+                        mJsonWriter.name("h").value(keyboardKey.mHeight);
+                        mJsonWriter.endObject();
+                    }
+                    mJsonWriter.endArray();
+                } else if (value instanceof SuggestedWords) {
+                    SuggestedWords words = (SuggestedWords) value;
+                    mJsonWriter.beginObject();
+                    mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
+                    mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect);
+                    mJsonWriter.name("isPunctuationSuggestions")
+                            .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();
+                    for (int j = 0; j < size; j++) {
+                        SuggestedWordInfo wordInfo = words.getWordInfo(j);
+                        mJsonWriter.value(wordInfo.toString());
+                    }
+                    mJsonWriter.endArray();
+                    mJsonWriter.endObject();
+                } else if (value == null) {
+                    mJsonWriter.nullValue();
+                } else {
+                    Log.w(TAG, "Unrecognized type to be logged: " +
+                            (value == null ? "<null>" : value.getClass().getName()));
+                    mJsonWriter.nullValue();
+                }
+            }
+            mJsonWriter.endObject();
+        } catch (IOException e) {
+            e.printStackTrace();
+            Log.w(TAG, "Error in JsonWriter; disabling logging");
+            try {
+                mJsonWriter.close();
+            } catch (IllegalStateException e1) {
+                // Assume that this is just the json not being terminated properly.
+                // Ignore
+            } catch (IOException e1) {
+                e1.printStackTrace();
+            } finally {
+                mJsonWriter = NULL_JSON_WRITER;
+            }
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
new file mode 100644
index 0000000..763fd6e
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -0,0 +1,1315 @@
+/*
+ * 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 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;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+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.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.Toast;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+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;
+import com.android.inputmethod.latin.RichInputConnection;
+import com.android.inputmethod.latin.RichInputConnection.Range;
+import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ * Logs the use of the LatinIME keyboard.
+ *
+ * This class logs operations on the IME keyboard, including what the user has typed.
+ * Data is stored locally in a file in app-specific storage.
+ *
+ * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
+ */
+public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+    private static final String TAG = ResearchLogger.class.getSimpleName();
+    private static final boolean DEBUG = false;
+    private static final boolean OUTPUT_ENTIRE_BUFFER = false;  // true may disclose private info
+    public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
+    /* package */ static boolean sIsLogging = false;
+    private static final int OUTPUT_FORMAT_VERSION = 1;
+    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+    private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
+    /* package */ static final String FILENAME_PREFIX = "researchLog";
+    private static final String FILENAME_SUFFIX = ".txt";
+    private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
+            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 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;
+    // 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.
+    // 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;
+    private SharedPreferences mPrefs;
+
+    // digits entered by the user are replaced with this codepoint.
+    /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
+            Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
+    // U+E001 is in the "private-use area"
+    /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
+    private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
+    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
+    private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
+    protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
+    // set when LatinIME should ignore an onUpdateSelection() callback that
+    // arises from operations in this class
+    private static boolean sLatinIMEExpectingUpdateSelection = false;
+
+    // used to check whether words are not unique
+    private Suggest mSuggest;
+    private Dictionary mDictionary;
+    private MainKeyboardView mMainKeyboardView;
+    private InputMethodService mInputMethodService;
+    private final Statistics mStatistics;
+
+    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) {
+        assert ims != null;
+        if (ims == null) {
+            Log.w(TAG, "IMS is null; logging is off");
+        } else {
+            mFilesDir = ims.getFilesDir();
+            if (mFilesDir == null || !mFilesDir.exists()) {
+                Log.w(TAG, "IME storage directory does not exist.");
+            }
+        }
+        if (prefs != null) {
+            mUUIDString = getUUID(prefs);
+            if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) {
+                Editor e = prefs.edit();
+                e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE);
+                e.apply();
+            }
+            sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+            prefs.registerOnSharedPreferenceChangeListener(this);
+
+            final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
+            final long now = System.currentTimeMillis();
+            if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) {
+                final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
+                cleanupLoggingDir(mFilesDir, timeHorizon);
+                Editor e = prefs.edit();
+                e.putLong(PREF_LAST_CLEANUP_TIME, now);
+                e.apply();
+            }
+        }
+        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) {
+        for (File file : dir.listFiles()) {
+            if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
+                    file.lastModified() < time) {
+                file.delete();
+            }
+        }
+    }
+
+    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);
+    }
+
+    private Dialog mSplashDialog = null;
+
+    private void maybeShowSplashScreen() {
+        if (hasSeenSplash()) {
+            return;
+        }
+        if (mSplashDialog != null && mSplashDialog.isShowing()) {
+            return;
+        }
+        final IBinder windowToken = mMainKeyboardView != null
+                ? mMainKeyboardView.getWindowToken() : null;
+        if (windowToken == null) {
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mInputMethodService)
+                .setTitle(R.string.research_splash_title)
+                .setMessage(R.string.research_splash_content)
+                .setPositiveButton(android.R.string.yes,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                onUserLoggingConsent();
+                                mSplashDialog.dismiss();
+                            }
+                })
+                .setNegativeButton(android.R.string.no,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                final String packageName = mInputMethodService.getPackageName();
+                                final Uri packageUri = Uri.parse("package:" + packageName);
+                                final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
+                                        packageUri);
+                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                                mInputMethodService.startActivity(intent);
+                            }
+                })
+                .setCancelable(true)
+                .setOnCancelListener(
+                        new OnCancelListener() {
+                            @Override
+                            public void onCancel(DialogInterface dialog) {
+                                mInputMethodService.requestHideSelf(0);
+                            }
+                });
+        mSplashDialog = builder.create();
+        final Window w = mSplashDialog.getWindow();
+        final WindowManager.LayoutParams lp = w.getAttributes();
+        lp.token = windowToken;
+        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+        w.setAttributes(lp);
+        w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+        mSplashDialog.show();
+    }
+
+    public void onUserLoggingConsent() {
+        setLoggingAllowed(true);
+        if (mPrefs == null) {
+            return;
+        }
+        final Editor e = mPrefs.edit();
+        e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true);
+        e.apply();
+        restart();
+    }
+
+    private void setLoggingAllowed(boolean enableLogging) {
+        if (mPrefs == null) {
+            return;
+        }
+        Editor e = mPrefs.edit();
+        e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
+        e.apply();
+        sIsLogging = enableLogging;
+    }
+
+    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 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() {
+        if (DEBUG) {
+            Log.d(TAG, "start called");
+        }
+        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() {
+        if (DEBUG) {
+            Log.d(TAG, "stop called");
+        }
+        logStatistics();
+        commitCurrentLogUnit();
+
+        if (mMainLogBuffer != null) {
+            publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */);
+            mMainResearchLog.close(null /* callback */);
+            mMainLogBuffer = null;
+        }
+        if (mFeedbackLogBuffer != null) {
+            mFeedbackLog.close(null /* callback */);
+            mFeedbackLogBuffer = null;
+        }
+    }
+
+    public boolean abort() {
+        if (DEBUG) {
+            Log.d(TAG, "abort called");
+        }
+        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() {
+        stop();
+        start();
+    }
+
+    private long mResumeTime = 0L;
+    private void suspendLoggingUntil(long time) {
+        mIsLoggingSuspended = true;
+        mResumeTime = time;
+        requestIndicatorRedraw();
+    }
+
+    private void resumeLogging() {
+        mResumeTime = 0L;
+        updateSuspendedState();
+        requestIndicatorRedraw();
+        if (isAllowedToLog()) {
+            restart();
+        }
+    }
+
+    private void updateSuspendedState() {
+        final long time = System.currentTimeMillis();
+        if (time > mResumeTime) {
+            mIsLoggingSuspended = false;
+        }
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+        if (key == null || prefs == null) {
+            return;
+        }
+        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+        if (sIsLogging == false) {
+            abort();
+        }
+        requestIndicatorRedraw();
+        mPrefs = prefs;
+        prefsChanged(prefs);
+    }
+
+    public void onResearchKeySelected(final LatinIME latinIME) {
+        if (mInFeedbackDialog) {
+            Toast.makeText(latinIME, R.string.research_please_exit_feedback_form,
+                    Toast.LENGTH_LONG).show();
+            return;
+        }
+        presentFeedbackDialog(latinIME);
+    }
+
+    // TODO: currently unreachable.  Remove after being sure no menu is needed.
+    /*
+    public void presentResearchDialog(final LatinIME latinIME) {
+        final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
+        final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
+        final CharSequence[] items = new CharSequence[] {
+                latinIME.getString(R.string.research_feedback_menu_option),
+                showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
+                        latinIME.getString(R.string.research_do_not_log_this_session)
+        };
+        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface di, int position) {
+                di.dismiss();
+                switch (position) {
+                    case 0:
+                        presentFeedbackDialog(latinIME);
+                        break;
+                    case 1:
+                        enableOrDisable(showEnable, latinIME);
+                        break;
+                }
+            }
+
+        };
+        final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
+                .setItems(items, listener)
+                .setTitle(title);
+        latinIME.showOptionDialog(builder.create());
+    }
+    */
+
+    private boolean mInFeedbackDialog = false;
+    public void presentFeedbackDialog(LatinIME latinIME) {
+        mInFeedbackDialog = true;
+        latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
+    }
+
+    // TODO: currently unreachable.  Remove after being sure enable/disable is
+    // not needed.
+    /*
+    public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) {
+        if (showEnable) {
+            if (!sIsLogging) {
+                setLoggingAllowed(true);
+            }
+            resumeLogging();
+            Toast.makeText(latinIME,
+                    R.string.research_notify_session_logging_enabled,
+                    Toast.LENGTH_LONG).show();
+        } else {
+            Toast toast = Toast.makeText(latinIME,
+                    R.string.research_notify_session_log_deleting,
+                    Toast.LENGTH_LONG);
+            toast.show();
+            boolean isLogDeleted = abort();
+            final long currentTime = System.currentTimeMillis();
+            final long resumeTime = currentTime + 1000 * 60 *
+                    SUSPEND_DURATION_IN_MINUTES;
+            suspendLoggingUntil(resumeTime);
+            toast.cancel();
+            Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+    */
+
+    private static final String[] EVENTKEYS_FEEDBACK = {
+        "UserTimestamp", "contents"
+    };
+    public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
+        if (mFeedbackLogBuffer == null) {
+            return;
+        }
+        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(new Runnable() {
+            @Override
+            public void run() {
+                uploadNow();
+            }
+        });
+        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
+    }
+
+    public void uploadNow() {
+        if (DEBUG) {
+            Log.d(TAG, "calling uploadNow()");
+        }
+        mInputMethodService.startService(mUploadIntent);
+    }
+
+    public void onLeavingSendFeedbackDialog() {
+        mInFeedbackDialog = false;
+    }
+
+    public void initSuggest(Suggest suggest) {
+        mSuggest = suggest;
+        if (mMainLogBuffer != null) {
+            mMainLogBuffer.setSuggest(mSuggest);
+        }
+    }
+
+    private void setIsPasswordView(boolean isPasswordView) {
+        mIsPasswordView = isPasswordView;
+    }
+
+    private boolean isAllowedToLog() {
+        if (DEBUG) {
+            Log.d(TAG, "iatl: " +
+                "mipw=" + mIsPasswordView +
+                ", mils=" + mIsLoggingSuspended +
+                ", sil=" + sIsLogging +
+                ", mInFeedbackDialog=" + mInFeedbackDialog);
+        }
+        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
+    }
+
+    public void requestIndicatorRedraw() {
+        if (!IS_SHOWING_INDICATOR) {
+            return;
+        }
+        if (mMainKeyboardView == null) {
+            return;
+        }
+        mMainKeyboardView.invalidateAllKeys();
+    }
+
+
+    public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
+            int height) {
+        // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
+        // and remove this method.
+        // The check for MainKeyboardView ensures that a red border is only placed around
+        // the main keyboard, not every keyboard.
+        if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
+            final int savedColor = paint.getColor();
+            paint.setColor(Color.RED);
+            final Style savedStyle = paint.getStyle();
+            paint.setStyle(Style.STROKE);
+            final float savedStrokeWidth = paint.getStrokeWidth();
+            if (IS_SHOWING_INDICATOR_CLEARLY) {
+                paint.setStrokeWidth(5);
+                canvas.drawRect(0, 0, width, height, paint);
+            } else {
+                // Put a tiny red dot on the screen so a knowledgeable user can check whether
+                // it is enabled.  The dot is actually a zero-width, zero-height rectangle,
+                // placed at the lower-right corner of the canvas, painted with a non-zero border
+                // width.
+                paint.setStrokeWidth(3);
+                canvas.drawRect(width, height, width, height, paint);
+            }
+            paint.setColor(savedColor);
+            paint.setStyle(savedStyle);
+            paint.setStrokeWidth(savedStrokeWidth);
+        }
+    }
+
+    private static final Object[] EVENTKEYS_NULLVALUES = {};
+
+    /**
+     * Buffer a research log event, flagging it as privacy-sensitive.
+     *
+     * This event contains potentially private information.  If the word that this event is a part
+     * of is determined to be privacy-sensitive, then this event should not be included in the
+     * output log.  The system waits to output until the containing word is known.
+     *
+     * @param keys an array containing a descriptive name for the event, followed by the keys
+     * @param values an array of values, either a String or Number.  length should be one
+     * less than the keys array
+     */
+    private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys,
+            final Object[] values) {
+        assert values.length + 1 == keys.length;
+        if (isAllowedToLog()) {
+            mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */);
+        }
+    }
+
+    private void setCurrentLogUnitContainsDigitFlag() {
+        mCurrentLogUnit.setContainsDigit();
+    }
+
+    /**
+     * Buffer a research log event, flaggint it as not privacy-sensitive.
+     *
+     * This event contains no potentially private information.  Even if the word that this event
+     * is privacy-sensitive, this event can still safely be sent to the output log.  The system
+     * waits until the containing word is known so that this event can be written in the proper
+     * temporal order with other events that may be privacy sensitive.
+     *
+     * @param keys an array containing a descriptive name for the event, followed by the keys
+     * @param values an array of values, either a String or Number.  length should be one
+     * less than the keys array
+     */
+    private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
+        assert values.length + 1 == keys.length;
+        if (isAllowedToLog()) {
+            mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */);
+        }
+    }
+
+    /* package for test */ void commitCurrentLogUnit() {
+        if (DEBUG) {
+            Log.d(TAG, "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");
+        }
+    }
+
+    /* 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) {
+        return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
+    }
+
+    /* package for test */ static String scrubDigitsFromString(String s) {
+        StringBuilder sb = null;
+        final int length = s.length();
+        for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
+            final int codePoint = Character.codePointAt(s, i);
+            if (Character.isDigit(codePoint)) {
+                if (sb == null) {
+                    sb = new StringBuilder(length);
+                    sb.append(s.substring(0, i));
+                }
+                sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
+            } else {
+                if (sb != null) {
+                    sb.appendCodePoint(codePoint);
+                }
+            }
+        }
+        if (sb == null) {
+            return s;
+        } else {
+            return sb.toString();
+        }
+    }
+
+    private static String getUUID(final SharedPreferences prefs) {
+        String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
+        if (null == uuidString) {
+            UUID uuid = UUID.randomUUID();
+            uuidString = uuid.toString();
+            Editor editor = prefs.edit();
+            editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
+            editor.apply();
+        }
+        return uuidString;
+    }
+
+    private String scrubWord(String word) {
+        if (mDictionary == null) {
+            return WORD_REPLACEMENT_STRING;
+        }
+        if (mDictionary.isValidWord(word)) {
+            return word;
+        }
+        return WORD_REPLACEMENT_STRING;
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = {
+        "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
+        "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion"
+    };
+    public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
+            final SharedPreferences prefs) {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.start();
+        if (editorInfo != null) {
+            final Context context = researchLogger.mInputMethodService;
+            try {
+                final PackageInfo packageInfo;
+                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
+                        0);
+                final Integer versionCode = packageInfo.versionCode;
+                final String versionName = packageInfo.versionName;
+                final Object[] values = {
+                        researchLogger.mUUIDString, editorInfo.packageName,
+                        Integer.toHexString(editorInfo.inputType),
+                        Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
+                        Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
+                        OUTPUT_FORMAT_VERSION
+                };
+                researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values);
+            } catch (NameNotFoundException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public void latinIME_onFinishInputInternal() {
+        stop();
+    }
+
+    private static final String[] EVENTKEYS_USER_FEEDBACK = {
+        "UserFeedback", "FeedbackContents"
+    };
+
+    private static final String[] EVENTKEYS_PREFS_CHANGED = {
+        "PrefsChanged", "prefs"
+    };
+    public static void prefsChanged(final SharedPreferences prefs) {
+        final ResearchLogger researchLogger = getInstance();
+        final Object[] values = {
+            prefs
+        };
+        researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values);
+    }
+
+    // Regular logging methods
+
+    private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
+        "MainKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size",
+        "pressure"
+    };
+    public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
+            final long eventTime, final int index, final int id, final int x, final int y) {
+        if (me != null) {
+            final String actionString;
+            switch (action) {
+                case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
+                case MotionEvent.ACTION_UP: actionString = "UP"; break;
+                case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
+                case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
+                case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
+                case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
+                case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
+                default: actionString = "ACTION_" + action; break;
+            }
+            final float size = me.getSize(index);
+            final float pressure = me.getPressure(index);
+            final Object[] values = {
+                actionString, eventTime, id, x, y, size, pressure
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT, values);
+        }
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONCODEINPUT = {
+        "LatinIMEOnCodeInput", "code", "x", "y"
+    };
+    public static void latinIME_onCodeInput(final int code, final int x, final int y) {
+        final long time = SystemClock.uptimeMillis();
+        final ResearchLogger researchLogger = getInstance();
+        final Object[] values = {
+            Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
+        };
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+        if (Character.isDigit(code)) {
+            researchLogger.setCurrentLogUnitContainsDigitFlag();
+        }
+        researchLogger.mStatistics.recordChar(code, time);
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
+        "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions"
+    };
+    public static void latinIME_onDisplayCompletions(
+            final CompletionInfo[] applicationSpecifiedCompletions) {
+        final Object[] values = {
+            applicationSpecifiedCompletions
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS,
+                values);
+    }
+
+    public static boolean getAndClearLatinIMEExpectingUpdateSelection() {
+        boolean returnValue = sLatinIMEExpectingUpdateSelection;
+        sLatinIMEExpectingUpdateSelection = false;
+        return returnValue;
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONWINDOWHIDDEN = {
+        "LatinIMEOnWindowHidden", "isTextTruncated", "text"
+    };
+    public static void latinIME_onWindowHidden(final int savedSelectionStart,
+            final int savedSelectionEnd, final InputConnection ic) {
+        if (ic != null) {
+            // 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);
+            ic.setSelection(savedSelectionStart, savedSelectionEnd);
+            ic.endBatchEdit();
+            sLatinIMEExpectingUpdateSelection = true;
+            final Object[] values = new Object[2];
+            if (OUTPUT_ENTIRE_BUFFER) {
+                if (TextUtils.isEmpty(charSequence)) {
+                    values[0] = false;
+                    values[1] = "";
+                } else {
+                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
+                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
+                        // do not cut in the middle of a supplementary character
+                        final char c = charSequence.charAt(length - 1);
+                        if (Character.isHighSurrogate(c)) {
+                            length--;
+                        }
+                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
+                                length);
+                        values[0] = true;
+                        values[1] = truncatedCharSequence.toString();
+                    } else {
+                        values[0] = false;
+                        values[1] = charSequence.toString();
+                    }
+                }
+            } else {
+                values[0] = true;
+                values[1] = "";
+            }
+            final ResearchLogger researchLogger = getInstance();
+            researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
+            researchLogger.commitCurrentLogUnit();
+            getInstance().stop();
+        }
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = {
+        "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart",
+        "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd",
+        "expectingUpdateSelection", "expectingUpdateSelectionFromLogger", "context"
+    };
+    public static void latinIME_onUpdateSelection(final int lastSelectionStart,
+            final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
+            final int newSelStart, final int newSelEnd, final int composingSpanStart,
+            final int composingSpanEnd, final boolean expectingUpdateSelection,
+            final boolean expectingUpdateSelectionFromLogger,
+            final RichInputConnection connection) {
+        String word = "";
+        if (connection != null) {
+            Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
+            if (range != null) {
+                word = range.mWord;
+            }
+        }
+        final ResearchLogger researchLogger = getInstance();
+        final String scrubbedWord = researchLogger.scrubWord(word);
+        final Object[] values = {
+            lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart,
+            newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection,
+            expectingUpdateSelectionFromLogger, scrubbedWord
+        };
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = {
+        "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y"
+    };
+    public static void latinIME_pickSuggestionManually(final String replacedWord,
+            final int index, CharSequence suggestion) {
+        final Object[] values = {
+            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,
+                values);
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = {
+        "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y"
+    };
+    public static void latinIME_punctuationSuggestion(final int index,
+            final CharSequence suggestion) {
+        final Object[] values = {
+            index, suggestion,
+            Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE
+        };
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = {
+        "LatinIMESendKeyCodePoint", "code"
+    };
+    public static void latinIME_sendKeyCodePoint(final int code) {
+        final Object[] values = {
+            Keyboard.printableCode(scrubDigitFromCodePoint(code))
+        };
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
+        if (Character.isDigit(code)) {
+            researchLogger.setCurrentLogUnitContainsDigitFlag();
+        }
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = {
+        "LatinIMESwapSwapperAndSpace"
+    };
+    public static void latinIME_swapSwapperAndSpace() {
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES);
+    }
+
+    private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = {
+        "MainKeyboardViewOnLongPress"
+    };
+    public static void mainKeyboardView_onLongPress() {
+        getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES);
+    }
+
+    private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD = {
+        "MainKeyboardViewSetKeyboard", "elementId", "locale", "orientation", "width",
+        "modeName", "action", "navigateNext", "navigatePrevious", "clobberSettingsKey",
+        "passwordInput", "shortcutKeyEnabled", "hasShortcutKey", "languageSwitchKeyEnabled",
+        "isMultiLine", "tw", "th", "keys"
+    };
+    public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
+        if (keyboard != null) {
+            final KeyboardId kid = keyboard.mId;
+            final boolean isPasswordView = kid.passwordInput();
+            getInstance().setIsPasswordView(isPasswordView);
+            final Object[] values = {
+                KeyboardId.elementIdToName(kid.mElementId),
+                kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
+                kid.mOrientation,
+                kid.mWidth,
+                KeyboardId.modeName(kid.mMode),
+                kid.imeAction(),
+                kid.navigateNext(),
+                kid.navigatePrevious(),
+                kid.mClobberSettingsKey,
+                isPasswordView,
+                kid.mShortcutKeyEnabled,
+                kid.mHasShortcutKey,
+                kid.mLanguageSwitchKeyEnabled,
+                kid.isMultiLine(),
+                keyboard.mOccupiedWidth,
+                keyboard.mOccupiedHeight,
+                keyboard.mKeys
+            };
+            getInstance().setIsPasswordView(isPasswordView);
+            getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD, values);
+        }
+    }
+
+    private static final String[] EVENTKEYS_LATINIME_REVERTCOMMIT = {
+        "LatinIMERevertCommit", "originallyTypedWord"
+    };
+    public static void latinIME_revertCommit(final String originallyTypedWord) {
+        final Object[] values = {
+            originallyTypedWord
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values);
+    }
+
+    private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = {
+        "PointerTrackerCallListenerOnCancelInput"
+    };
+    public static void pointerTracker_callListenerOnCancelInput() {
+        getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT,
+                EVENTKEYS_NULLVALUES);
+    }
+
+    private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT = {
+        "PointerTrackerCallListenerOnCodeInput", "code", "outputText", "x", "y",
+        "ignoreModifierKey", "altersCode", "isEnabled"
+    };
+    public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
+            final int y, final boolean ignoreModifierKey, final boolean altersCode,
+            final int code) {
+        if (key != null) {
+            String outputText = key.getOutputText();
+            final Object[] values = {
+                Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null
+                        : scrubDigitsFromString(outputText.toString()),
+                x, y, ignoreModifierKey, altersCode, key.isEnabled()
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values);
+        }
+    }
+
+    private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = {
+        "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey",
+        "isEnabled"
+    };
+    public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
+            final boolean withSliding, final boolean ignoreModifierKey) {
+        if (key != null) {
+            final Object[] values = {
+                Keyboard.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
+                ignoreModifierKey, key.isEnabled()
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values);
+        }
+    }
+
+    private static final String[] EVENTKEYS_POINTERTRACKER_ONDOWNEVENT = {
+        "PointerTrackerOnDownEvent", "deltaT", "distanceSquared"
+    };
+    public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
+        final Object[] values = {
+            deltaT, distanceSquared
+        };
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values);
+    }
+
+    private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = {
+        "PointerTrackerOnMoveEvent", "x", "y", "lastX", "lastY"
+    };
+    public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
+            final int lastY) {
+        final Object[] values = {
+            x, y, lastX, lastY
+        };
+        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"
+    };
+    public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
+        if (me != null) {
+            final Object[] values = {
+                me.toString()
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values);
+        }
+    }
+
+    private static final String[] EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = {
+        "SuggestionStripViewSetSuggestions", "suggestedWords"
+    };
+    public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
+        if (suggestedWords != null) {
+            final Object[] values = {
+                suggestedWords
+            };
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, values);
+        }
+    }
+
+    private static final String[] EVENTKEYS_USER_TIMESTAMP = {
+        "UserTimestamp"
+    };
+    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 d53757f..567648f 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -15,17 +15,18 @@
 LOCAL_PATH := $(call my-dir)
 
 ############ some local flags
-# If you change any of those flags, you need to rebuild both libjni_latinime_static
-# and the shared library.
-#FLAG_DBG := true
-#FLAG_DO_PROFILE := true
+# If you change any of those flags, you need to rebuild both libjni_latinime_common_static
+# and the shared library that uses libjni_latinime_common_static.
+FLAG_DBG ?= false
+FLAG_DO_PROFILE ?= false
 
 ######################################
 include $(CLEAR_VARS)
 
 LATIN_IME_SRC_DIR := src
+LATIN_IME_SRC_FULLPATH_DIR := $(LOCAL_PATH)/$(LATIN_IME_SRC_DIR)
 
-LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(LATIN_IME_SRC_DIR)
+LOCAL_C_INCLUDES += $(LATIN_IME_SRC_FULLPATH_DIR) $(LATIN_IME_SRC_FULLPATH_DIR)/gesture
 
 LOCAL_CFLAGS += -Werror -Wall
 
@@ -35,6 +36,7 @@
 LATIN_IME_JNI_SRC_FILES := \
     com_android_inputmethod_keyboard_ProximityInfo.cpp \
     com_android_inputmethod_latin_BinaryDictionary.cpp \
+    com_android_inputmethod_latin_DicTraverseSession.cpp \
     jni_common.cpp
 
 LATIN_IME_CORE_SRC_FILES := \
@@ -44,12 +46,16 @@
     char_utils.cpp \
     correction.cpp \
     dictionary.cpp \
+    dic_traverse_wrapper.cpp \
     proximity_info.cpp \
-    unigram_dictionary.cpp
+    proximity_info_state.cpp \
+    unigram_dictionary.cpp \
+    gesture/gesture_decoder_wrapper.cpp \
+    gesture/incremental_decoder_wrapper.cpp
 
 LOCAL_SRC_FILES := \
     $(LATIN_IME_JNI_SRC_FILES) \
-    $(addprefix $(LATIN_IME_SRC_DIR)/,$(LATIN_IME_CORE_SRC_FILES))
+    $(addprefix $(LATIN_IME_SRC_DIR)/, $(LATIN_IME_CORE_SRC_FILES))
 
 ifeq ($(FLAG_DO_PROFILE), true)
     $(warning Making profiling version of native library)
@@ -61,50 +67,40 @@
 endif # FLAG_DBG
 endif # FLAG_DO_PROFILE
 
-LOCAL_MODULE := libjni_latinime_static
+LOCAL_MODULE := libjni_latinime_common_static
 LOCAL_MODULE_TAGS := optional
 
-ifdef HISTORICAL_NDK_VERSIONS_ROOT # In the platform build system
-include external/stlport/libstlport.mk
-else # In the NDK build system
-LOCAL_C_INCLUDES += external/stlport/stlport bionic
-endif
+LOCAL_SDK_VERSION := 14
+LOCAL_NDK_STL_VARIANT := stlport_static
 
 include $(BUILD_STATIC_LIBRARY)
-
 ######################################
 include $(CLEAR_VARS)
 
 # All code in LOCAL_WHOLE_STATIC_LIBRARIES will be built into this shared library.
-LOCAL_WHOLE_STATIC_LIBRARIES := libjni_latinime_static
-
-ifdef HISTORICAL_NDK_VERSIONS_ROOT # In the platform build system
-LOCAL_SHARED_LIBRARIES := libstlport
-else # In the NDK build system
-LOCAL_SHARED_LIBRARIES := libstlport_static
-endif
+LOCAL_WHOLE_STATIC_LIBRARIES := libjni_latinime_common_static
 
 ifeq ($(FLAG_DO_PROFILE), true)
     $(warning Making profiling version of native library)
-    LOCAL_SHARED_LIBRARIES += libcutils libutils
+    LOCAL_SHARED_LIBRARIES += liblog
 else # FLAG_DO_PROFILE
 ifeq ($(FLAG_DBG), true)
     $(warning Making debug version of native library)
-    LOCAL_SHARED_LIBRARIES += libcutils libutils
+    LOCAL_SHARED_LIBRARIES += liblog
 endif # FLAG_DBG
 endif # FLAG_DO_PROFILE
 
 LOCAL_MODULE := libjni_latinime
 LOCAL_MODULE_TAGS := optional
 
-ifdef HISTORICAL_NDK_VERSIONS_ROOT # In the platform build system
-include external/stlport/libstlport.mk
-endif
+LOCAL_SDK_VERSION := 14
+LOCAL_NDK_STL_VARIANT := stlport_static
 
 include $(BUILD_SHARED_LIBRARY)
 
 #################### Clean up the tmp vars
 LATIN_IME_CORE_SRC_FILES :=
 LATIN_IME_JNI_SRC_FILES :=
+LATIN_IME_GESTURE_IMPL_SRC_FILES :=
 LATIN_IME_SRC_DIR :=
-TARGETING_UNBUNDLED_FROYO :=
+LATIN_IME_SRC_FULLPATH_DIR :=
diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
index 9eb437c..560b3a5 100644
--- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
+++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
@@ -1,19 +1,18 @@
 /*
-**
-** 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.
-*/
+ * 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.
+ */
 
 #define LOG_TAG "LatinIME: jni: ProximityInfo"
 
@@ -22,62 +21,30 @@
 #include "jni_common.h"
 #include "proximity_info.h"
 
-#include <assert.h>
-#include <errno.h>
-#include <stdio.h>
-#include <string>
-
 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) {
@@ -85,5 +52,4 @@
     return registerNativeMethods(env, kClassPathName, sKeyboardMethods,
             sizeof(sKeyboardMethods) / sizeof(sKeyboardMethods[0]));
 }
-
 } // namespace latinime
diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h
index 4a1e83b..51fa895 100644
--- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h
+++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h
@@ -1,19 +1,18 @@
 /*
-**
-** 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.
-*/
+ * 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.
+ */
 
 #ifndef _COM_ANDROID_INPUTMETHOD_KEYBOARD_PROXIMITYINFO_H
 #define _COM_ANDROID_INPUTMETHOD_KEYBOARD_PROXIMITYINFO_H
@@ -24,6 +23,5 @@
 
 int register_ProximityInfo(JNIEnv *env);
 
-}
-
+} // namespace latinime
 #endif // _COM_ANDROID_INPUTMETHOD_KEYBOARD_PROXIMITYINFO_H
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index d10dc96..dd2513f 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -1,59 +1,62 @@
 /*
-**
-** Copyright 2009, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
+ * Copyright (C) 2009, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include <cstring> // for memset()
 
 #define LOG_TAG "LatinIME: jni: BinaryDictionary"
 
+#include "defines.h" // for macros below
+
+#ifdef USE_MMAP_FOR_DICTIONARY
+#include <cerrno>
+#include <fcntl.h>
+#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 "correction.h"
 #include "com_android_inputmethod_latin_BinaryDictionary.h"
-#include "defines.h"
+#include "correction.h"
 #include "dictionary.h"
 #include "jni.h"
 #include "jni_common.h"
-#include "proximity_info.h"
-
-#include <assert.h>
-#include <errno.h>
-#include <stdio.h>
-
-#ifdef USE_MMAP_FOR_DICTIONARY
-#include <sys/mman.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-#include <unistd.h>
-#else // USE_MMAP_FOR_DICTIONARY
-#include <stdlib.h>
-#endif // USE_MMAP_FOR_DICTIONARY
 
 namespace latinime {
 
-void releaseDictBuf(void* dictBuf, const size_t length, int fd);
+class ProximityInfo;
+
+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,
-        jint typedLetterMultiplier, jint fullWordMultiplier, jint maxWordLength, jint maxWords) {
+        jint typedLetterMultiplier, jint fullWordMultiplier, jint maxWordLength, jint maxWords,
+        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;
@@ -65,15 +68,15 @@
         return 0;
     }
     int pagesize = getpagesize();
-    adjust = dictOffset % pagesize;
-    int adjDictOffset = dictOffset - adjust;
-    int adjDictSize = dictSize + adjust;
+    adjust = static_cast<int>(dictOffset) % pagesize;
+    int adjDictOffset = static_cast<int>(dictOffset) - adjust;
+    int adjDictSize = static_cast<int>(dictSize) + adjust;
     dictBuf = mmap(0, sizeof(char) * adjDictSize, PROT_READ, MAP_PRIVATE, fd, adjDictOffset);
     if (dictBuf == MAP_FAILED) {
         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;
@@ -103,23 +106,22 @@
         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
     } else {
-        dictionary = new Dictionary(dictBuf, dictSize, fd, adjust, typedLetterMultiplier,
-                fullWordMultiplier, maxWordLength, maxWords);
+        dictionary = new Dictionary(dictBuf, static_cast<int>(dictSize), fd, adjust,
+                typedLetterMultiplier, fullWordMultiplier, maxWordLength, maxWords, maxPredictions);
     }
     PROF_END(66);
     PROF_CLOSE;
@@ -127,105 +129,131 @@
 }
 
 static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jobject object, jlong dict,
-        jlong proximityInfo, jintArray xCoordinatesArray, jintArray yCoordinatesArray,
-        jintArray inputArray, jint arraySize, jintArray prevWordForBigrams,
-        jboolean useFullEditDistance, jcharArray outputArray, jintArray frequencyArray) {
-    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 *frequencies = env->GetIntArrayElements(frequencyArray, 0);
-    int *inputCodes = env->GetIntArrayElements(inputArray, 0);
-    jchar *outputChars = env->GetCharArrayElements(outputArray, 0);
-    jint *prevWordChars = prevWordForBigrams
-            ? env->GetIntArrayElements(prevWordForBigrams, 0) : 0;
-    jsize prevWordLength = prevWordChars ? env->GetArrayLength(prevWordForBigrams) : 0;
-    int count = dictionary->getSuggestions(pInfo, xCoordinates, yCoordinates, inputCodes,
-            arraySize, prevWordChars, prevWordLength, useFullEditDistance,
-            (unsigned short*) outputChars, frequencies);
-    if (prevWordChars) {
-        env->ReleaseIntArrayElements(prevWordForBigrams, prevWordChars, JNI_ABORT);
-    }
-    env->ReleaseCharArrayElements(outputArray, outputChars, 0);
-    env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT);
-    env->ReleaseIntArrayElements(frequencyArray, frequencies, 0);
-    env->ReleaseIntArrayElements(yCoordinatesArray, yCoordinates, 0);
-    env->ReleaseIntArrayElements(xCoordinatesArray, xCoordinates, 0);
-    return count;
-}
+    ProximityInfo *pInfo = reinterpret_cast<ProximityInfo *>(proximityInfo);
+    void *traverseSession = reinterpret_cast<void *>(dicTraverseSession);
 
-static int latinime_BinaryDictionary_getBigrams(JNIEnv *env, jobject object, jlong dict,
-        jintArray prevWordArray, jint prevWordLength, jintArray inputArray, jint inputArraySize,
-        jcharArray outputArray, jintArray frequencyArray, jint maxWordLength, jint maxBigrams) {
-    Dictionary *dictionary = (Dictionary*)dict;
-    if (!dictionary) return 0;
-    jint *prevWord = env->GetIntArrayElements(prevWordArray, 0);
-    int *inputCodes = env->GetIntArrayElements(inputArray, 0);
-    jchar *outputChars = env->GetCharArrayElements(outputArray, 0);
-    int *frequencies = env->GetIntArrayElements(frequencyArray, 0);
-    int count = dictionary->getBigrams(prevWord, prevWordLength, inputCodes,
-            inputArraySize, (unsigned short*) outputChars, frequencies, maxWordLength, maxBigrams);
-    env->ReleaseIntArrayElements(frequencyArray, frequencies, 0);
-    env->ReleaseCharArrayElements(outputArray, outputChars, 0);
-    env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT);
-    env->ReleaseIntArrayElements(prevWordArray, prevWord, JNI_ABORT);
+    // 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 > 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(prevWordCodePoints, prevWordCodePointsLength,
+                inputCodePoints, arraySize, outputChars, scores, outputTypes);
+    }
+
+    // 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);
@@ -233,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);
     }
@@ -244,27 +272,29 @@
         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;JJIIII)J", (void*)latinime_BinaryDictionary_open},
-    {"closeNative", "(J)V", (void*)latinime_BinaryDictionary_close},
-    {"getSuggestionsNative", "(JJ[I[I[II[IZ[C[I)I",
-            (void*)latinime_BinaryDictionary_getSuggestions},
-    {"getFrequencyNative", "(J[II)I", (void*)latinime_BinaryDictionary_getFrequency},
-    {"isValidBigramNative", "(J[I[I)Z", (void*)latinime_BinaryDictionary_isValidBigram},
-    {"getBigramsNative", "(J[II[II[C[III)I", (void*)latinime_BinaryDictionary_getBigrams},
-    {"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) {
-    const char* const kClassPathName = "com/android/inputmethod/latin/BinaryDictionary";
+    const char *const kClassPathName = "com/android/inputmethod/latin/BinaryDictionary";
     return registerNativeMethods(env, kClassPathName, sMethods,
             sizeof(sMethods) / sizeof(sMethods[0]));
 }
-
 } // namespace latinime
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.h b/native/jni/com_android_inputmethod_latin_BinaryDictionary.h
index 1b1ba7f..b9e944f 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.h
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.h
@@ -1,19 +1,18 @@
 /*
-**
-** 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.
-*/
+ * 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.
+ */
 
 #ifndef _COM_ANDROID_INPUTMETHOD_LATIN_BINARYDICTIONARY_H
 #define _COM_ANDROID_INPUTMETHOD_LATIN_BINARYDICTIONARY_H
@@ -24,6 +23,5 @@
 
 int register_BinaryDictionary(JNIEnv *env);
 
-}
-
+} // namespace latinime
 #endif // _COM_ANDROID_INPUTMETHOD_LATIN_BINARYDICTIONARY_H
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_DicTraverseSession.h b/native/jni/com_android_inputmethod_latin_DicTraverseSession.h
new file mode 100644
index 0000000..37531e9
--- /dev/null
+++ b/native/jni/com_android_inputmethod_latin_DicTraverseSession.h
@@ -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.
+ */
+
+#ifndef _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
+#define _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
+
+#include "defines.h"
+#include "jni.h"
+
+namespace latinime {
+int register_DicTraverseSession(JNIEnv *env);
+} // namespace latinime
+#endif // _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
diff --git a/native/jni/jni_common.cpp b/native/jni/jni_common.cpp
index b9e2c32..0da1669 100644
--- a/native/jni/jni_common.cpp
+++ b/native/jni/jni_common.cpp
@@ -1,59 +1,62 @@
 /*
-**
-** 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.
-*/
+ * 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.
+ */
 
 #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_DicTraverseSession.h"
 #include "defines.h"
 #include "jni.h"
-#include "proximity_info.h"
-
-#include <assert.h>
-#include <errno.h>
-#include <stdio.h>
+#include "jni_common.h"
 
 using namespace latinime;
 
 /*
  * Returns the JNI version on success, -1 on failure.
  */
-jint JNI_OnLoad(JavaVM* vm, void* reserved) {
-    JNIEnv* env = 0;
+jint JNI_OnLoad(JavaVM *vm, void *reserved) {
+    JNIEnv *env = 0;
     jint result = -1;
 
-    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
         AKLOGE("ERROR: GetEnv failed");
         goto bail;
     }
-    assert(env != 0);
+    assert(env);
 
     if (!register_BinaryDictionary(env)) {
         AKLOGE("ERROR: BinaryDictionary native registration failed");
         goto bail;
     }
 
+    if (!register_DicTraverseSession(env)) {
+        AKLOGE("ERROR: DicTraverseSession native registration failed");
+        goto bail;
+    }
+
     if (!register_ProximityInfo(env)) {
         AKLOGE("ERROR: ProximityInfo native registration failed");
         goto bail;
     }
 
     /* success -- return valid version number */
-    result = JNI_VERSION_1_4;
+    result = JNI_VERSION_1_6;
 
 bail:
     return result;
@@ -61,10 +64,10 @@
 
 namespace latinime {
 
-int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* methods,
+int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods,
         int numMethods) {
     jclass clazz = env->FindClass(className);
-    if (clazz == 0) {
+    if (!clazz) {
         AKLOGE("Native registration unable to find class '%s'", className);
         return JNI_FALSE;
     }
@@ -76,5 +79,4 @@
     env->DeleteLocalRef(clazz);
     return JNI_TRUE;
 }
-
 } // namespace latinime
diff --git a/native/jni/jni_common.h b/native/jni/jni_common.h
index 6741443..993f97e 100644
--- a/native/jni/jni_common.h
+++ b/native/jni/jni_common.h
@@ -1,25 +1,22 @@
 /*
-**
-** 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.
-*/
+ * 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.
+ */
 
 #ifndef LATINIME_JNI_COMMON_H
 #define LATINIME_JNI_COMMON_H
 
-#include <stdlib.h>
-
 #include "jni.h"
 
 namespace latinime {
@@ -27,34 +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 224f020..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'
@@ -38,4 +40,4 @@
 const int32_t AdditionalProximityChars::EN_US_ADDITIONAL_U[EN_US_ADDITIONAL_U_SIZE] = {
     'a', 'e', 'i', 'o'
 };
-}
+} // namespace latinime
diff --git a/native/jni/src/additional_proximity_chars.h b/native/jni/src/additional_proximity_chars.h
index e0ecc0e..d420c46 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"
 
@@ -26,7 +26,8 @@
 
 class AdditionalProximityChars {
  private:
-    static const std::string LOCALE_EN_US;
+    DISALLOW_IMPLICIT_CONSTRUCTORS(AdditionalProximityChars);
+    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;
@@ -38,17 +39,18 @@
     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) {
+        switch (c) {
         case 'a':
             return EN_US_ADDITIONAL_A_SIZE;
         case 'e':
@@ -64,11 +66,11 @@
         }
     }
 
-    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) {
+        switch (c) {
         case 'a':
             return EN_US_ADDITIONAL_A;
         case 'e':
@@ -83,12 +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/basechars.cpp b/native/jni/src/basechars.cpp
index 31f1e18..379cb62 100644
--- a/native/jni/src/basechars.cpp
+++ b/native/jni/src/basechars.cpp
@@ -14,17 +14,19 @@
  * limitations under the License.
  */
 
+#include <stdint.h>
+
 #include "char_utils.h"
 
 namespace latinime {
 
-/**
+/*
  * Table mapping most combined Latin, Greek, and Cyrillic characters
  * to their base characters.  If c is in range, BASE_CHARS[c] == c
  * if c is not a combined character, or the base character if it
  * is combined.
  */
-const unsigned short BASE_CHARS[BASE_CHARS_SIZE] = {
+const uint16_t BASE_CHARS[BASE_CHARS_SIZE] = {
     0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
     0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
     0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
@@ -187,8 +189,6 @@
     0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7,
     0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff,
 };
-
 // generated with:
 // cat UnicodeData.txt | perl -e 'while (<>) { @foo = split(/;/); $foo[5] =~ s/<.*> //; $base[hex($foo[0])] = hex($foo[5]);} for ($i = 0; $i < 0x500; $i += 8) { for ($j = $i; $j < $i + 8; $j++) { printf("0x%04x, ", $base[$j] ? $base[$j] : $j)}; print "\n"; }'
-
 } // namespace latinime
diff --git a/native/jni/src/bigram_dictionary.cpp b/native/jni/src/bigram_dictionary.cpp
index 9ef024d..dade4f1 100644
--- a/native/jni/src/bigram_dictionary.cpp
+++ b/native/jni/src/bigram_dictionary.cpp
@@ -1,21 +1,20 @@
 /*
-**
-** Copyright 2010, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
+ * Copyright (C) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-#include <string.h>
+#include <cstring>
 
 #define LOG_TAG "LatinIME: bigram_dictionary.cpp"
 
@@ -27,9 +26,8 @@
 
 namespace latinime {
 
-BigramDictionary::BigramDictionary(const unsigned char *dict, int maxWordLength,
-        Dictionary *parentDictionary)
-    : DICT(dict), MAX_WORD_LENGTH(maxWordLength), mParentDictionary(parentDictionary) {
+BigramDictionary::BigramDictionary(const unsigned char *dict, int maxWordLength, int maxPredictions)
+        : DICT(dict), MAX_WORD_LENGTH(maxWordLength), MAX_PREDICTIONS(maxPredictions) {
     if (DEBUG_DICT) {
         AKLOGI("BigramDictionary - constructor");
     }
@@ -38,7 +36,8 @@
 BigramDictionary::~BigramDictionary() {
 }
 
-bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequency) {
+bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequency,
+        int *bigramFreq, unsigned short *bigramChars, int *outputTypes) const {
     word[length] = 0;
     if (DEBUG_DICT) {
 #ifdef FLAG_DBG
@@ -50,25 +49,26 @@
 
     // Find the right insertion point
     int insertAt = 0;
-    while (insertAt < mMaxBigrams) {
-        if (frequency > mBigramFreq[insertAt] || (mBigramFreq[insertAt] == frequency
-                && length < Dictionary::wideStrLen(mBigramChars + insertAt * MAX_WORD_LENGTH))) {
+    while (insertAt < MAX_PREDICTIONS) {
+        if (frequency > bigramFreq[insertAt] || (bigramFreq[insertAt] == frequency
+                && length < Dictionary::wideStrLen(bigramChars + insertAt * MAX_WORD_LENGTH))) {
             break;
         }
         insertAt++;
     }
     if (DEBUG_DICT) {
-        AKLOGI("Bigram: InsertAt -> %d maxBigrams: %d", insertAt, mMaxBigrams);
+        AKLOGI("Bigram: InsertAt -> %d MAX_PREDICTIONS: %d", insertAt, MAX_PREDICTIONS);
     }
-    if (insertAt < mMaxBigrams) {
-        memmove((char*) mBigramFreq + (insertAt + 1) * sizeof(mBigramFreq[0]),
-               (char*) mBigramFreq + insertAt * sizeof(mBigramFreq[0]),
-               (mMaxBigrams - insertAt - 1) * sizeof(mBigramFreq[0]));
-        mBigramFreq[insertAt] = frequency;
-        memmove((char*) mBigramChars + (insertAt + 1) * MAX_WORD_LENGTH * sizeof(short),
-               (char*) mBigramChars + (insertAt    ) * MAX_WORD_LENGTH * sizeof(short),
-               (mMaxBigrams - insertAt - 1) * sizeof(short) * MAX_WORD_LENGTH);
-        unsigned short *dest = mBigramChars + (insertAt    ) * MAX_WORD_LENGTH;
+    if (insertAt < MAX_PREDICTIONS) {
+        memmove(bigramFreq + (insertAt + 1),
+                bigramFreq + insertAt,
+                (MAX_PREDICTIONS - insertAt - 1) * sizeof(bigramFreq[0]));
+        bigramFreq[insertAt] = frequency;
+        outputTypes[insertAt] = Dictionary::KIND_PREDICTION;
+        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++;
         }
@@ -84,12 +84,11 @@
 /* Parameters :
  * prevWord: the word before, the one for which we need to look up bigrams.
  * prevWordLength: its length.
- * codes: what user typed, in the same format as for UnigramDictionary::getSuggestions.
+ * inputCodes: what user typed, in the same format as for UnigramDictionary::getSuggestions.
  * codesSize: the size of the codes array.
  * bigramChars: an array for output, at the same format as outwords for getSuggestions.
  * bigramFreq: an array to output frequencies.
- * maxWordLength: the maximum size of a word.
- * maxBigrams: the maximum number of bigrams fitting in the bigramChars array.
+ * outputTypes: an array to output types.
  * This method returns the number of bigrams this word has, for backward compatibility.
  * Note: this is not the number of bigrams output in the array, which is the number of
  * bigrams this word has WHOSE first letter also matches the letter the user typed.
@@ -98,21 +97,23 @@
  * and the bigrams are used to boost unigram result scores, it makes little sense to
  * reduce their scope to the ones that match the first letter.
  */
-int BigramDictionary::getBigrams(const int32_t *prevWord, int prevWordLength, int *codes,
-        int codesSize, unsigned short *bigramChars, int *bigramFreq, int maxWordLength,
-        int maxBigrams) {
+int BigramDictionary::getBigrams(const int32_t *prevWord, int prevWordLength, int *inputCodes,
+        int codesSize, unsigned short *bigramChars, int *bigramFreq, int *outputTypes) const {
     // TODO: remove unused arguments, and refrain from storing stuff in members of this class
     // TODO: have "in" arguments before "out" ones, and make out args explicit in the name
-    mBigramFreq = bigramFreq;
-    mBigramChars = bigramChars;
-    mInputCodes = codes;
-    mMaxBigrams = maxBigrams;
 
-    const uint8_t* const root = DICT;
-    int pos = getBigramListPositionForWord(prevWord, prevWordLength);
+    const uint8_t *const root = DICT;
+    int pos = getBigramListPositionForWord(prevWord, prevWordLength,
+            false /* forceLowerCaseSearch */);
     // getBigramListPositionForWord returns 0 if this word isn't in the dictionary or has no bigrams
+    if (0 == pos) {
+        // If no bigrams for this exact word, search again in lower case.
+        pos = getBigramListPositionForWord(prevWord, prevWordLength,
+                true /* forceLowerCaseSearch */);
+    }
+    // If still no bigrams, we really don't have them!
     if (0 == pos) return 0;
-    int bigramFlags;
+    uint8_t bigramFlags;
     int bigramCount = 0;
     do {
         bigramFlags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
@@ -124,36 +125,38 @@
                 bigramBuffer, &unigramFreq);
 
         // codesSize == 0 means we are trying to find bigram predictions.
-        if (codesSize < 1 || checkFirstCharacter(bigramBuffer)) {
-            const int bigramFreq = UnigramDictionary::MASK_ATTRIBUTE_FREQUENCY & bigramFlags;
+        if (codesSize < 1 || checkFirstCharacter(bigramBuffer, inputCodes)) {
+            const int bigramFreqTemp = BinaryFormat::MASK_ATTRIBUTE_FREQUENCY & bigramFlags;
             // Due to space constraints, the frequency for bigrams is approximate - the lower the
             // unigram frequency, the worse the precision. The theoritical maximum error in
             // resulting frequency is 8 - although in the practice it's never bigger than 3 or 4
             // in very bad cases. This means that sometimes, we'll see some bigrams interverted
             // here, but it can't get too bad.
             const int frequency =
-                    BinaryFormat::computeFrequencyForBigram(unigramFreq, bigramFreq);
-            if (addWordBigram(bigramBuffer, length, frequency)) {
+                    BinaryFormat::computeFrequencyForBigram(unigramFreq, bigramFreqTemp);
+            if (addWordBigram(bigramBuffer, length, frequency, bigramFreq, bigramChars,
+                    outputTypes)) {
                 ++bigramCount;
             }
         }
-    } while (UnigramDictionary::FLAG_ATTRIBUTE_HAS_NEXT & bigramFlags);
+    } while (BinaryFormat::FLAG_ATTRIBUTE_HAS_NEXT & bigramFlags);
     return bigramCount;
 }
 
 // Returns a pointer to the start of the bigram list.
 // If the word is not found or has no bigrams, this function returns 0.
 int BigramDictionary::getBigramListPositionForWord(const int32_t *prevWord,
-        const int prevWordLength) {
+        const int prevWordLength, const bool forceLowerCaseSearch) const {
     if (0 >= prevWordLength) return 0;
-    const uint8_t* const root = DICT;
-    int pos = BinaryFormat::getTerminalPosition(root, prevWord, prevWordLength);
+    const uint8_t *const root = DICT;
+    int pos = BinaryFormat::getTerminalPosition(root, prevWord, prevWordLength,
+            forceLowerCaseSearch);
 
     if (NOT_VALID_WORD == pos) return 0;
-    const int flags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
-    if (0 == (flags & UnigramDictionary::FLAG_HAS_BIGRAMS)) return 0;
-    if (0 == (flags & UnigramDictionary::FLAG_HAS_MULTIPLE_CHARS)) {
-        BinaryFormat::getCharCodeAndForwardPointer(root, &pos);
+    const uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
+    if (0 == (flags & BinaryFormat::FLAG_HAS_BIGRAMS)) return 0;
+    if (0 == (flags & BinaryFormat::FLAG_HAS_MULTIPLE_CHARS)) {
+        BinaryFormat::getCodePointAndForwardPointer(root, &pos);
     } else {
         pos = BinaryFormat::skipOtherCharacters(root, pos);
     }
@@ -164,28 +167,33 @@
 }
 
 void BigramDictionary::fillBigramAddressToFrequencyMapAndFilter(const int32_t *prevWord,
-        const int prevWordLength, std::map<int, int> *map, uint8_t *filter) {
+        const int prevWordLength, std::map<int, int> *map, uint8_t *filter) const {
     memset(filter, 0, BIGRAM_FILTER_BYTE_SIZE);
-    const uint8_t* const root = DICT;
-    int pos = getBigramListPositionForWord(prevWord, prevWordLength);
+    const uint8_t *const root = DICT;
+    int pos = getBigramListPositionForWord(prevWord, prevWordLength,
+            false /* forceLowerCaseSearch */);
+    if (0 == pos) {
+        // If no bigrams for this exact string, search again in lower case.
+        pos = getBigramListPositionForWord(prevWord, prevWordLength,
+                true /* forceLowerCaseSearch */);
+    }
     if (0 == pos) return;
 
-    int bigramFlags;
+    uint8_t bigramFlags;
     do {
         bigramFlags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
-        const int frequency = UnigramDictionary::MASK_ATTRIBUTE_FREQUENCY & bigramFlags;
+        const int frequency = BinaryFormat::MASK_ATTRIBUTE_FREQUENCY & bigramFlags;
         const int bigramPos = BinaryFormat::getAttributeAddressAndForwardPointer(root, bigramFlags,
                 &pos);
         (*map)[bigramPos] = frequency;
         setInFilter(filter, bigramPos);
-    } while (0 != (UnigramDictionary::FLAG_ATTRIBUTE_HAS_NEXT & bigramFlags));
+    } while (0 != (BinaryFormat::FLAG_ATTRIBUTE_HAS_NEXT & bigramFlags));
 }
 
-bool BigramDictionary::checkFirstCharacter(unsigned short *word) {
+bool BigramDictionary::checkFirstCharacter(unsigned short *word, int *inputCodes) const {
     // Checks whether this word starts with same character or neighboring characters of
     // what user typed.
 
-    int *inputCodes = mInputCodes;
     int maxAlt = MAX_ALTERNATIVES;
     const unsigned short firstBaseChar = toBaseLowerCase(*word);
     while (maxAlt > 0) {
@@ -199,14 +207,15 @@
 }
 
 bool BigramDictionary::isValidBigram(const int32_t *word1, int length1, const int32_t *word2,
-        int length2) {
-    const uint8_t* const root = DICT;
-    int pos = getBigramListPositionForWord(word1, length1);
+        int length2) const {
+    const uint8_t *const root = DICT;
+    int pos = getBigramListPositionForWord(word1, length1, false /* forceLowerCaseSearch */);
     // getBigramListPositionForWord returns 0 if this word isn't in the dictionary or has no bigrams
     if (0 == pos) return false;
-    int nextWordPos = BinaryFormat::getTerminalPosition(root, word2, length2);
+    int nextWordPos = BinaryFormat::getTerminalPosition(root, word2, length2,
+            false /* forceLowerCaseSearch */);
     if (NOT_VALID_WORD == nextWordPos) return false;
-    int bigramFlags;
+    uint8_t bigramFlags;
     do {
         bigramFlags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
         const int bigramPos = BinaryFormat::getAttributeAddressAndForwardPointer(root, bigramFlags,
@@ -214,7 +223,7 @@
         if (bigramPos == nextWordPos) {
             return true;
         }
-    } while (UnigramDictionary::FLAG_ATTRIBUTE_HAS_NEXT & bigramFlags);
+    } while (BinaryFormat::FLAG_ATTRIBUTE_HAS_NEXT & bigramFlags);
     return false;
 }
 
diff --git a/native/jni/src/bigram_dictionary.h b/native/jni/src/bigram_dictionary.h
index b8763a5..5f11ae8 100644
--- a/native/jni/src/bigram_dictionary.h
+++ b/native/jni/src/bigram_dictionary.h
@@ -24,39 +24,33 @@
 
 namespace latinime {
 
-class Dictionary;
 class BigramDictionary {
  public:
-    BigramDictionary(const unsigned char *dict, int maxWordLength, Dictionary *parentDictionary);
-    int getBigrams(const int32_t *word, int length, int *codes, int codesSize,
-            unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams);
-    int getBigramListPositionForWord(const int32_t *prevWord, const int prevWordLength);
+    BigramDictionary(const unsigned char *dict, int maxWordLength, int maxPredictions);
+    int getBigrams(const int32_t *word, int length, int *inputCodes, int codesSize,
+            unsigned short *outWords, int *frequencies, int *outputTypes) const;
     void fillBigramAddressToFrequencyMapAndFilter(const int32_t *prevWord, const int prevWordLength,
-            std::map<int, int> *map, uint8_t *filter);
-    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2);
+            std::map<int, int> *map, uint8_t *filter) const;
+    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2) const;
     ~BigramDictionary();
  private:
-    bool addWordBigram(unsigned short *word, int length, int frequency);
+    DISALLOW_IMPLICIT_CONSTRUCTORS(BigramDictionary);
+    bool addWordBigram(unsigned short *word, int length, int frequency,
+            int *bigramFreq, unsigned short *bigramChars, int *outputTypes) const;
     int getBigramAddress(int *pos, bool advance);
     int getBigramFreq(int *pos);
     void searchForTerminalNode(int addressLookingFor, int frequency);
     bool getFirstBitOfByte(int *pos) { return (DICT[*pos] & 0x80) > 0; }
     bool getSecondBitOfByte(int *pos) { return (DICT[*pos] & 0x40) > 0; }
-    bool checkFirstCharacter(unsigned short *word);
+    bool checkFirstCharacter(unsigned short *word, int *inputCodes) const;
+    int getBigramListPositionForWord(const int32_t *prevWord, const int prevWordLength,
+            const bool forceLowerCaseSearch) const;
 
     const unsigned char *DICT;
     const int MAX_WORD_LENGTH;
+    const int MAX_PREDICTIONS;
     // TODO: Re-implement proximity correction for bigram correction
     static const int MAX_ALTERNATIVES = 1;
-
-    Dictionary *mParentDictionary;
-    int *mBigramFreq;
-    int mMaxBigrams;
-    unsigned short *mBigramChars;
-    int *mInputCodes;
-    int mInputLength;
 };
-
 } // namespace latinime
-
 #endif // LATINIME_BIGRAM_DICTIONARY_H
diff --git a/native/jni/src/binary_format.h b/native/jni/src/binary_format.h
index 51bf8eb..eec52e3 100644
--- a/native/jni/src/binary_format.h
+++ b/native/jni/src/binary_format.h
@@ -18,18 +18,53 @@
 #define LATINIME_BINARY_FORMAT_H
 
 #include <limits>
+#include <map>
 #include "bloom_filter.h"
-#include "unigram_dictionary.h"
+#include "char_utils.h"
 
 namespace latinime {
 
 class BinaryFormat {
- private:
-    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:
+    // Mask and flags for children address type selection.
+    static const int MASK_GROUP_ADDRESS_TYPE = 0xC0;
+    static const int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
+    static const int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
+    static const int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
+    static const int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+
+    // Flag for single/multiple char group
+    static const int FLAG_HAS_MULTIPLE_CHARS = 0x20;
+
+    // Flag for terminal groups
+    static const int FLAG_IS_TERMINAL = 0x10;
+
+    // Flag for shortcut targets presence
+    static const int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
+    // Flag for bigram presence
+    static const int FLAG_HAS_BIGRAMS = 0x04;
+    // Flag for non-words (typically, shortcut only entries)
+    static const int FLAG_IS_NOT_A_WORD = 0x02;
+    // Flag for blacklist
+    static const int FLAG_IS_BLACKLISTED = 0x01;
+
+    // Attribute (bigram/shortcut) related flags:
+    // Flag for presence of more attributes
+    static const int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
+    // Flag for sign of offset. If this flag is set, the offset value must be negated.
+    static const int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
+
+    // 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;
+    static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
+    static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
+    static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
+
     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
@@ -44,29 +79,29 @@
     const static int CHARACTER_ARRAY_TERMINATOR_SIZE = 1;
     const static int SHORTCUT_LIST_SIZE_SIZE = 2;
 
-    static int detectFormat(const uint8_t* const dict);
-    static unsigned int getHeaderSize(const uint8_t* const dict);
-    static unsigned int getFlags(const uint8_t* const dict);
-    static int getGroupCountAndForwardPointer(const uint8_t* const dict, int* pos);
-    static uint8_t getFlagsAndForwardPointer(const uint8_t* const dict, int* pos);
-    static int32_t getCharCodeAndForwardPointer(const uint8_t* const dict, int* pos);
-    static int readFrequencyWithoutMovingPointer(const uint8_t* const dict, const int pos);
-    static int skipOtherCharacters(const uint8_t* const dict, const int pos);
+    static int detectFormat(const uint8_t *const dict);
+    static unsigned int getHeaderSize(const uint8_t *const dict);
+    static unsigned int getFlags(const uint8_t *const dict);
+    static int getGroupCountAndForwardPointer(const uint8_t *const dict, int *pos);
+    static uint8_t getFlagsAndForwardPointer(const uint8_t *const dict, int *pos);
+    static int32_t getCodePointAndForwardPointer(const uint8_t *const dict, int *pos);
+    static int readFrequencyWithoutMovingPointer(const uint8_t *const dict, const int pos);
+    static int skipOtherCharacters(const uint8_t *const dict, const int pos);
     static int skipChildrenPosition(const uint8_t flags, const int pos);
     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,
+    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 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 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,
+    static int getAttributeAddressAndForwardPointer(const uint8_t *const dict, const uint8_t flags,
             int *pos);
-    static int getTerminalPosition(const uint8_t* const root, const int32_t* const inWord,
-            const int length);
-    static int getWordAtAddress(const uint8_t* const root, const int address, const int maxDepth,
-            uint16_t* outWord, int* outUnigramFrequency);
+    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,
+            uint16_t *outWord, int *outUnigramFrequency);
     static int computeFrequencyForBigram(const int unigramFreq, const int bigramFreq);
     static int getProbability(const int position, const std::map<int, int> *bigramMap,
             const uint8_t *bigramFilter, const int unigramFreq);
@@ -79,9 +114,16 @@
         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) {
+inline int BinaryFormat::detectFormat(const uint8_t *const dict) {
     // The magic number is stored big-endian.
     const uint32_t magicNumber = (dict[0] << 24) + (dict[1] << 16) + (dict[2] << 8) + dict[3];
     switch (magicNumber) {
@@ -103,7 +145,7 @@
     }
 }
 
-inline unsigned int BinaryFormat::getFlags(const uint8_t* const dict) {
+inline unsigned int BinaryFormat::getFlags(const uint8_t *const dict) {
     switch (detectFormat(dict)) {
     case 1:
         return NO_FLAGS;
@@ -112,7 +154,7 @@
     }
 }
 
-inline unsigned int BinaryFormat::getHeaderSize(const uint8_t* const dict) {
+inline unsigned int BinaryFormat::getHeaderSize(const uint8_t *const dict) {
     switch (detectFormat(dict)) {
     case 1:
         return FORMAT_VERSION_1_HEADER_SIZE;
@@ -124,41 +166,41 @@
     }
 }
 
-inline int BinaryFormat::getGroupCountAndForwardPointer(const uint8_t* const dict, int* pos) {
+inline int BinaryFormat::getGroupCountAndForwardPointer(const uint8_t *const dict, int *pos) {
     const int msb = dict[(*pos)++];
     if (msb < 0x80) return msb;
     return ((msb & 0x7F) << 8) | dict[(*pos)++];
 }
 
-inline uint8_t BinaryFormat::getFlagsAndForwardPointer(const uint8_t* const dict, int* pos) {
+inline uint8_t BinaryFormat::getFlagsAndForwardPointer(const uint8_t *const dict, int *pos) {
     return dict[(*pos)++];
 }
 
-inline int32_t BinaryFormat::getCharCodeAndForwardPointer(const uint8_t* const dict, int* pos) {
+inline int32_t BinaryFormat::getCodePointAndForwardPointer(const uint8_t *const dict, int *pos) {
     const int origin = *pos;
-    const int32_t character = dict[origin];
-    if (character < MINIMAL_ONE_BYTE_CHARACTER_VALUE) {
-        if (character == CHARACTER_ARRAY_TERMINATOR) {
+    const int32_t codePoint = dict[origin];
+    if (codePoint < MINIMAL_ONE_BYTE_CHARACTER_VALUE) {
+        if (codePoint == CHARACTER_ARRAY_TERMINATOR) {
             *pos = origin + 1;
-            return NOT_A_CHARACTER;
+            return NOT_A_CODE_POINT;
         } else {
             *pos = origin + 3;
-            const int32_t char_1 = character << 16;
+            const int32_t char_1 = codePoint << 16;
             const int32_t char_2 = char_1 + (dict[origin + 1] << 8);
             return char_2 + dict[origin + 2];
         }
     } else {
         *pos = origin + 1;
-        return character;
+        return codePoint;
     }
 }
 
-inline int BinaryFormat::readFrequencyWithoutMovingPointer(const uint8_t* const dict,
+inline int BinaryFormat::readFrequencyWithoutMovingPointer(const uint8_t *const dict,
         const int pos) {
     return dict[pos];
 }
 
-inline int BinaryFormat::skipOtherCharacters(const uint8_t* const dict, const int pos) {
+inline int BinaryFormat::skipOtherCharacters(const uint8_t *const dict, const int pos) {
     int currentPos = pos;
     int32_t character = dict[currentPos++];
     while (CHARACTER_ARRAY_TERMINATOR != character) {
@@ -172,22 +214,22 @@
 
 static inline int attributeAddressSize(const uint8_t flags) {
     static const int ATTRIBUTE_ADDRESS_SHIFT = 4;
-    return (flags & UnigramDictionary::MASK_ATTRIBUTE_ADDRESS_TYPE) >> ATTRIBUTE_ADDRESS_SHIFT;
+    return (flags & BinaryFormat::MASK_ATTRIBUTE_ADDRESS_TYPE) >> ATTRIBUTE_ADDRESS_SHIFT;
     /* Note: this is a value-dependant optimization of what may probably be
        more readably written this way:
-       switch (flags * UnigramDictionary::MASK_ATTRIBUTE_ADDRESS_TYPE) {
-       case UnigramDictionary::FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE: return 1;
-       case UnigramDictionary::FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES: return 2;
-       case UnigramDictionary::FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTE: return 3;
+       switch (flags * BinaryFormat::MASK_ATTRIBUTE_ADDRESS_TYPE) {
+       case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE: return 1;
+       case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES: return 2;
+       case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTE: return 3;
        default: return 0;
        }
     */
 }
 
-static inline int skipExistingBigrams(const uint8_t* const dict, const int pos) {
+static inline int skipExistingBigrams(const uint8_t *const dict, const int pos) {
     int currentPos = pos;
     uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(dict, &currentPos);
-    while (flags & UnigramDictionary::FLAG_ATTRIBUTE_HAS_NEXT) {
+    while (flags & BinaryFormat::FLAG_ATTRIBUTE_HAS_NEXT) {
         currentPos += attributeAddressSize(flags);
         flags = BinaryFormat::getFlagsAndForwardPointer(dict, &currentPos);
     }
@@ -197,11 +239,11 @@
 
 static inline int childrenAddressSize(const uint8_t flags) {
     static const int CHILDREN_ADDRESS_SHIFT = 6;
-    return (UnigramDictionary::MASK_GROUP_ADDRESS_TYPE & flags) >> CHILDREN_ADDRESS_SHIFT;
+    return (BinaryFormat::MASK_GROUP_ADDRESS_TYPE & flags) >> CHILDREN_ADDRESS_SHIFT;
     /* See the note in attributeAddressSize. The same applies here */
 }
 
-static inline int shortcutByteSize(const uint8_t* const dict, const int pos) {
+static inline int shortcutByteSize(const uint8_t *const dict, const int pos) {
     return ((int)(dict[pos] << 8)) + (dict[pos + 1]);
 }
 
@@ -210,28 +252,28 @@
 }
 
 inline int BinaryFormat::skipFrequency(const uint8_t flags, const int pos) {
-    return UnigramDictionary::FLAG_IS_TERMINAL & flags ? pos + 1 : pos;
+    return FLAG_IS_TERMINAL & flags ? pos + 1 : pos;
 }
 
-inline int BinaryFormat::skipShortcuts(const uint8_t* const dict, const uint8_t flags,
+inline int BinaryFormat::skipShortcuts(const uint8_t *const dict, const uint8_t flags,
         const int pos) {
-    if (UnigramDictionary::FLAG_HAS_SHORTCUT_TARGETS & flags) {
+    if (FLAG_HAS_SHORTCUT_TARGETS & flags) {
         return pos + shortcutByteSize(dict, pos);
     } else {
         return pos;
     }
 }
 
-inline int BinaryFormat::skipBigrams(const uint8_t* const dict, const uint8_t flags,
+inline int BinaryFormat::skipBigrams(const uint8_t *const dict, const uint8_t flags,
         const int pos) {
-    if (UnigramDictionary::FLAG_HAS_BIGRAMS & flags) {
+    if (FLAG_HAS_BIGRAMS & flags) {
         return skipExistingBigrams(dict, pos);
     } else {
         return pos;
     }
 }
 
-inline int BinaryFormat::skipAllAttributes(const uint8_t* const dict, const uint8_t flags,
+inline int BinaryFormat::skipAllAttributes(const uint8_t *const dict, const uint8_t flags,
         const int pos) {
     // This function skips all attributes: shortcuts and bigrams.
     int newPos = pos;
@@ -240,7 +282,7 @@
     return newPos;
 }
 
-inline int BinaryFormat::skipChildrenPosAndAttributes(const uint8_t* const dict,
+inline int BinaryFormat::skipChildrenPosAndAttributes(const uint8_t *const dict,
         const uint8_t flags, const int pos) {
     int currentPos = pos;
     currentPos = skipChildrenPosition(flags, currentPos);
@@ -248,18 +290,18 @@
     return currentPos;
 }
 
-inline int BinaryFormat::readChildrenPosition(const uint8_t* const dict, const uint8_t flags,
+inline int BinaryFormat::readChildrenPosition(const uint8_t *const dict, const uint8_t flags,
         const int pos) {
     int offset = 0;
-    switch (UnigramDictionary::MASK_GROUP_ADDRESS_TYPE & flags) {
-        case UnigramDictionary::FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
+    switch (MASK_GROUP_ADDRESS_TYPE & flags) {
+        case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
             offset = dict[pos];
             break;
-        case UnigramDictionary::FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
+        case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
             offset = dict[pos] << 8;
             offset += dict[pos + 1];
             break;
-        case UnigramDictionary::FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
+        case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
             offset = dict[pos] << 16;
             offset += dict[pos + 1] << 8;
             offset += dict[pos + 2];
@@ -273,74 +315,77 @@
 }
 
 inline bool BinaryFormat::hasChildrenInFlags(const uint8_t flags) {
-    return (UnigramDictionary::FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
-            != (UnigramDictionary::MASK_GROUP_ADDRESS_TYPE & flags));
+    return (FLAG_GROUP_ADDRESS_TYPE_NOADDRESS != (MASK_GROUP_ADDRESS_TYPE & flags));
 }
 
-inline int BinaryFormat::getAttributeAddressAndForwardPointer(const uint8_t* const dict,
+inline int BinaryFormat::getAttributeAddressAndForwardPointer(const uint8_t *const dict,
         const uint8_t flags, int *pos) {
     int offset = 0;
     const int origin = *pos;
-    switch (UnigramDictionary::MASK_ATTRIBUTE_ADDRESS_TYPE & flags) {
-        case UnigramDictionary::FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+    switch (MASK_ATTRIBUTE_ADDRESS_TYPE & flags) {
+        case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
             offset = dict[origin];
             *pos = origin + 1;
             break;
-        case UnigramDictionary::FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+        case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
             offset = dict[origin] << 8;
             offset += dict[origin + 1];
             *pos = origin + 2;
             break;
-        case UnigramDictionary::FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+        case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
             offset = dict[origin] << 16;
             offset += dict[origin + 1] << 8;
             offset += dict[origin + 2];
             *pos = origin + 3;
             break;
     }
-    if (UnigramDictionary::FLAG_ATTRIBUTE_OFFSET_NEGATIVE & flags) {
+    if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE & flags) {
         return origin - offset;
     } else {
         return origin + offset;
     }
 }
 
+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,
-        const int32_t* const inWord, const int length) {
+inline int BinaryFormat::getTerminalPosition(const uint8_t *const root,
+        const int32_t *const inWord, const int length, const bool forceLowerCaseSearch) {
     int pos = 0;
     int wordPos = 0;
 
     while (true) {
         // If we already traversed the tree further than the word is long, there means
         // there was no match (or we would have found it).
-        if (wordPos > length) return NOT_VALID_WORD;
+        if (wordPos >= length) return NOT_VALID_WORD;
         int charGroupCount = BinaryFormat::getGroupCountAndForwardPointer(root, &pos);
-        const int32_t wChar = inWord[wordPos];
+        const int32_t wChar = forceLowerCaseSearch ? toLowerCase(inWord[wordPos]) : inWord[wordPos];
         while (true) {
             // If there are no more character groups in this node, it means we could not
             // find a matching character for this depth, therefore there is no match.
             if (0 >= charGroupCount) return NOT_VALID_WORD;
             const int charGroupPos = pos;
             const uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
-            int32_t character = BinaryFormat::getCharCodeAndForwardPointer(root, &pos);
+            int32_t character = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
             if (character == wChar) {
                 // This is the correct node. Only one character group may start with the same
                 // char within a node, so either we found our match in this node, or there is
                 // no match and we can return NOT_VALID_WORD. So we will check all the characters
                 // in this character group indeed does match.
-                if (UnigramDictionary::FLAG_HAS_MULTIPLE_CHARS & flags) {
-                    character = BinaryFormat::getCharCodeAndForwardPointer(root, &pos);
-                    while (NOT_A_CHARACTER != character) {
+                if (FLAG_HAS_MULTIPLE_CHARS & flags) {
+                    character = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
+                    while (NOT_A_CODE_POINT != character) {
                         ++wordPos;
                         // If we shoot the length of the word we search for, or if we find a single
                         // character that does not match, as explained above, it means the word is
                         // not in the dictionary (by virtue of this chargroup being the only one to
                         // match the word on the first character, but not matching the whole word).
-                        if (wordPos > length) return NOT_VALID_WORD;
+                        if (wordPos >= length) return NOT_VALID_WORD;
                         if (inWord[wordPos] != character) return NOT_VALID_WORD;
-                        character = BinaryFormat::getCharCodeAndForwardPointer(root, &pos);
+                        character = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
                     }
                 }
                 // If we come here we know that so far, we do match. Either we are on a terminal
@@ -348,14 +393,13 @@
                 // If we don't match the length AND don't have children, then a word in the
                 // dictionary fully matches a prefix of the searched word but not the full word.
                 ++wordPos;
-                if (UnigramDictionary::FLAG_IS_TERMINAL & flags) {
+                if (FLAG_IS_TERMINAL & flags) {
                     if (wordPos == length) {
                         return charGroupPos;
                     }
-                    pos = BinaryFormat::skipFrequency(UnigramDictionary::FLAG_IS_TERMINAL, pos);
+                    pos = BinaryFormat::skipFrequency(FLAG_IS_TERMINAL, pos);
                 }
-                if (UnigramDictionary::FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
-                        == (UnigramDictionary::MASK_GROUP_ADDRESS_TYPE & flags)) {
+                if (FLAG_GROUP_ADDRESS_TYPE_NOADDRESS == (MASK_GROUP_ADDRESS_TYPE & flags)) {
                     return NOT_VALID_WORD;
                 }
                 // We have children and we are still shorter than the word we are searching for, so
@@ -365,7 +409,7 @@
                 break;
             } else {
                 // This chargroup does not match, so skip the remaining part and go to the next.
-                if (UnigramDictionary::FLAG_HAS_MULTIPLE_CHARS & flags) {
+                if (FLAG_HAS_MULTIPLE_CHARS & flags) {
                     pos = BinaryFormat::skipOtherCharacters(root, pos);
                 }
                 pos = BinaryFormat::skipFrequency(flags, pos);
@@ -394,8 +438,8 @@
  * outUnigramFrequency: a pointer to an int to write the frequency into.
  * Return value : the length of the word, of 0 if the word was not found.
  */
-inline int BinaryFormat::getWordAtAddress(const uint8_t* const root, const int address,
-        const int maxDepth, uint16_t* outWord, int* outUnigramFrequency) {
+inline int BinaryFormat::getWordAtAddress(const uint8_t *const root, const int address,
+        const int maxDepth, uint16_t *outWord, int *outUnigramFrequency) {
     int pos = 0;
     int wordPos = 0;
 
@@ -413,19 +457,19 @@
                  --charGroupCount) {
             const int startPos = pos;
             const uint8_t flags = getFlagsAndForwardPointer(root, &pos);
-            const int32_t character = getCharCodeAndForwardPointer(root, &pos);
+            const int32_t character = getCodePointAndForwardPointer(root, &pos);
             if (address == startPos) {
                 // We found the address. Copy the rest of the word in the buffer and return
                 // the length.
                 outWord[wordPos] = character;
-                if (UnigramDictionary::FLAG_HAS_MULTIPLE_CHARS & flags) {
-                    int32_t nextChar = getCharCodeAndForwardPointer(root, &pos);
+                if (FLAG_HAS_MULTIPLE_CHARS & flags) {
+                    int32_t nextChar = getCodePointAndForwardPointer(root, &pos);
                     // We count chars in order to avoid infinite loops if the file is broken or
                     // if there is some other bug
                     int charCount = maxDepth;
-                    while (NOT_A_CHARACTER != nextChar && --charCount > 0) {
+                    while (NOT_A_CODE_POINT != nextChar && --charCount > 0) {
                         outWord[++wordPos] = nextChar;
-                        nextChar = getCharCodeAndForwardPointer(root, &pos);
+                        nextChar = getCodePointAndForwardPointer(root, &pos);
                     }
                 }
                 *outUnigramFrequency = readFrequencyWithoutMovingPointer(root, pos);
@@ -433,7 +477,7 @@
             }
             // We need to skip past this char group, so skip any remaining chars after the
             // first and possibly the frequency.
-            if (UnigramDictionary::FLAG_HAS_MULTIPLE_CHARS & flags) {
+            if (FLAG_HAS_MULTIPLE_CHARS & flags) {
                 pos = skipOtherCharacters(root, pos);
             }
             pos = skipFrequency(flags, pos);
@@ -441,8 +485,8 @@
             // The fact that this group has children is very important. Since we already know
             // that this group does not match, if it has no children we know it is irrelevant
             // to what we are searching for.
-            const bool hasChildren = (UnigramDictionary::FLAG_GROUP_ADDRESS_TYPE_NOADDRESS !=
-                    (UnigramDictionary::MASK_GROUP_ADDRESS_TYPE & flags));
+            const bool hasChildren = (FLAG_GROUP_ADDRESS_TYPE_NOADDRESS !=
+                    (MASK_GROUP_ADDRESS_TYPE & flags));
             // We will write in `found' whether we have passed the children address we are
             // searching for. For example if we search for "beer", the children of b are less
             // than the address we are searching for and the children of c are greater. When we
@@ -479,16 +523,16 @@
                     const uint8_t lastFlags =
                             getFlagsAndForwardPointer(root, &lastCandidateGroupPos);
                     const int32_t lastChar =
-                            getCharCodeAndForwardPointer(root, &lastCandidateGroupPos);
+                            getCodePointAndForwardPointer(root, &lastCandidateGroupPos);
                     // We copy all the characters in this group to the buffer
                     outWord[wordPos] = lastChar;
-                    if (UnigramDictionary::FLAG_HAS_MULTIPLE_CHARS & lastFlags) {
+                    if (FLAG_HAS_MULTIPLE_CHARS & lastFlags) {
                         int32_t nextChar =
-                                getCharCodeAndForwardPointer(root, &lastCandidateGroupPos);
+                                getCodePointAndForwardPointer(root, &lastCandidateGroupPos);
                         int charCount = maxDepth;
                         while (-1 != nextChar && --charCount > 0) {
                             outWord[++wordPos] = nextChar;
-                            nextChar = getCharCodeAndForwardPointer(root, &lastCandidateGroupPos);
+                            nextChar = getCodePointAndForwardPointer(root, &lastCandidateGroupPos);
                         }
                     }
                     ++wordPos;
@@ -538,8 +582,8 @@
     // 0 for the bigram frequency represents the middle of the 16th step from the top,
     // while a value of 15 represents the middle of the top step.
     // See makedict.BinaryDictInputOutput for details.
-    const float stepSize = ((float)MAX_FREQ - unigramFreq) / (1.5f + MAX_BIGRAM_FREQ);
-    return (int)(unigramFreq + (bigramFreq + 1) * stepSize);
+    const float stepSize = static_cast<float>(MAX_FREQ - unigramFreq) / (1.5f + MAX_BIGRAM_FREQ);
+    return unigramFreq + static_cast<int>(static_cast<float>(bigramFreq + 1) * stepSize);
 }
 
 // This returns a probability in log space.
@@ -555,7 +599,5 @@
         return backoff(unigramFreq);
     }
 }
-
 } // namespace latinime
-
 #endif // LATINIME_BINARY_FORMAT_H
diff --git a/native/jni/src/bloom_filter.h b/native/jni/src/bloom_filter.h
index 7ae6a1f..bcce1f7 100644
--- a/native/jni/src/bloom_filter.h
+++ b/native/jni/src/bloom_filter.h
@@ -23,16 +23,16 @@
 
 namespace latinime {
 
-static inline void setInFilter(uint8_t *filter, const int position) {
-    const unsigned int bucket = position % BIGRAM_FILTER_MODULO;
-    filter[bucket >> 3] |= (1 << (bucket & 0x7));
+// TODO: uint32_t position
+static inline void setInFilter(uint8_t *filter, const int32_t position) {
+    const uint32_t bucket = static_cast<uint32_t>(position % BIGRAM_FILTER_MODULO);
+    filter[bucket >> 3] |= static_cast<uint8_t>(1 << (bucket & 0x7));
 }
 
-static inline bool isInFilter(const uint8_t *filter, const int position) {
-    const unsigned int bucket = position % BIGRAM_FILTER_MODULO;
-    return filter[bucket >> 3] & (1 << (bucket & 0x7));
+// TODO: uint32_t position
+static inline bool isInFilter(const uint8_t *filter, const int32_t position) {
+    const uint32_t bucket = static_cast<uint32_t>(position % BIGRAM_FILTER_MODULO);
+    return filter[bucket >> 3] & static_cast<uint8_t>(1 << (bucket & 0x7));
 }
-
 } // namespace latinime
-
 #endif // LATINIME_BLOOM_FILTER_H
diff --git a/native/jni/src/char_utils.cpp b/native/jni/src/char_utils.cpp
index a31a063..9d886da31 100644
--- a/native/jni/src/char_utils.cpp
+++ b/native/jni/src/char_utils.cpp
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-#include <stdlib.h>
+#include <cstdlib>
+
+#include "char_utils.h"
 
 namespace latinime {
 
@@ -883,17 +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 607dc51..b17f262 100644
--- a/native/jni/src/char_utils.h
+++ b/native/jni/src/char_utils.h
@@ -17,21 +17,24 @@
 #ifndef LATINIME_CHAR_UTILS_H
 #define LATINIME_CHAR_UTILS_H
 
+#include <cctype>
+#include <stdint.h>
+
 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
@@ -41,7 +44,7 @@
  */
 
 static const int BASE_CHARS_SIZE = 0x0500;
-extern const unsigned short BASE_CHARS[BASE_CHARS_SIZE];
+extern const uint16_t BASE_CHARS[BASE_CHARS_SIZE];
 
 inline static unsigned short toBaseChar(unsigned short c) {
     if (c < BASE_CHARS_SIZE) {
@@ -50,8 +53,7 @@
     return c;
 }
 
-inline static unsigned short toBaseLowerCase(unsigned short c) {
-    c = toBaseChar(c);
+inline static unsigned short toLowerCase(const unsigned short c) {
     if (isAsciiUpper(c)) {
         return toAsciiLower(c);
     } else if (isAscii(c)) {
@@ -60,6 +62,8 @@
     return latin_tolower(c);
 }
 
+inline static unsigned short toBaseLowerCase(const unsigned short c) {
+    return toLowerCase(toBaseChar(c));
+}
 } // namespace latinime
-
 #endif // LATINIME_CHAR_UTILS_H
diff --git a/native/jni/src/correction.cpp b/native/jni/src/correction.cpp
index 99f5b92..49e3e3c 100644
--- a/native/jni/src/correction.cpp
+++ b/native/jni/src/correction.cpp
@@ -14,22 +14,22 @@
  * limitations under the License.
  */
 
-#include <assert.h>
-#include <ctype.h>
-#include <math.h>
-#include <stdio.h>
-#include <string.h>
+#include <cassert>
+#include <cctype>
+#include <cmath>
+#include <cstring>
 
 #define LOG_TAG "LatinIME: correction.cpp"
 
 #include "char_utils.h"
 #include "correction.h"
 #include "defines.h"
-#include "dictionary.h"
-#include "proximity_info.h"
+#include "proximity_info_state.h"
 
 namespace latinime {
 
+class ProximityInfo;
+
 /////////////////////////////
 // edit distance funcitons //
 /////////////////////////////
@@ -55,25 +55,25 @@
             }
             AKLOGI("[ %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d ]",
                     c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10]);
-            (void)c;
+            (void)c; // To suppress compiler warning
         }
     }
 }
 
 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,42 +84,37 @@
 }
 
 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];
 }
 
 //////////////////////
 // inline functions //
 //////////////////////
-static const char QUOTE = '\'';
+static const char SINGLE_QUOTE = '\'';
 
-inline bool Correction::isQuote(const unsigned short c) {
-    const unsigned short userTypedChar = mProximityInfo->getPrimaryCharAt(mInputIndex);
-    return (c == QUOTE && userTypedChar != QUOTE);
+inline bool Correction::isSingleQuote(const unsigned short c) {
+    const unsigned short userTypedChar = mProximityInfoState.getPrimaryCharAt(mInputIndex);
+    return (c == SINGLE_QUOTE && userTypedChar != SINGLE_QUOTE);
 }
 
 ////////////////
 // Correction //
 ////////////////
 
-Correction::Correction(const int typedLetterMultiplier, const int fullWordMultiplier)
-        : TYPED_LETTER_MULTIPLIER(typedLetterMultiplier), FULL_WORD_MULTIPLIER(fullWordMultiplier) {
-    initEditDistance(mEditDistanceTable);
-}
-
 void Correction::resetCorrection() {
     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);
@@ -159,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,
@@ -171,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;
 }
 
@@ -233,7 +226,7 @@
 }
 
 // TODO: remove
-int Correction::getInputIndex() {
+int Correction::getInputIndex() const {
     return mInputIndex;
 }
 
@@ -277,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 = mProximityInfo->getPrimaryInputWord();
-    calcEditDistanceOneStep(mEditDistanceTable, primaryInputWord, mInputLength,
+    const unsigned short *primaryInputWord = mProximityInfoState.getPrimaryInputWord();
+    calcEditDistanceOneStep(mEditDistanceTable, primaryInputWord, mInputSize,
             mWord, mOutputIndex + 1);
 }
 
@@ -308,13 +301,12 @@
     return UNRELATED;
 }
 
-inline bool isEquivalentChar(ProximityInfo::ProximityType type) {
-    return type == ProximityInfo::EQUIVALENT_CHAR;
+inline bool isEquivalentChar(ProximityType type) {
+    return type == EQUIVALENT_CHAR;
 }
 
-inline bool isProximityCharOrEquivalentChar(ProximityInfo::ProximityType type) {
-    return type == ProximityInfo::EQUIVALENT_CHAR
-            || type == ProximityInfo::NEAR_PROXIMITY_CHAR;
+inline bool isProximityCharOrEquivalentChar(ProximityType type) {
+    return type == EQUIVALENT_CHAR || type == NEAR_PROXIMITY_CHAR;
 }
 
 Correction::CorrectionType Correction::processCharAndCalcState(
@@ -331,25 +323,25 @@
     mDistances[mOutputIndex] = NOT_A_DISTANCE;
 
     // Skip checking this node
-    if (mNeedsToTraverseAllNodes || isQuote(c)) {
+    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 ProximityInfo::ProximityType matchId =
-                    mProximityInfo->getMatchedProximityId(mInputIndex, c, true, &proximityIndex);
+            const ProximityType matchId = mProximityInfoState.getMatchedProximityId(
+                    mInputIndex, c, true, &proximityIndex);
             if (isEquivalentChar(matchId)) {
                 mLastCharExceeded = false;
                 --mExcessiveCount;
                 mDistances[mOutputIndex] =
-                        mProximityInfo->getNormalizedSquaredDistance(mInputIndex, 0);
-            } else if (matchId == ProximityInfo::NEAR_PROXIMITY_CHAR) {
+                        mProximityInfoState.getNormalizedSquaredDistance(mInputIndex, 0);
+            } else if (matchId == NEAR_PROXIMITY_CHAR) {
                 mLastCharExceeded = false;
                 --mExcessiveCount;
                 ++mProximityCount;
-                mDistances[mOutputIndex] =
-                        mProximityInfo->getNormalizedSquaredDistance(mInputIndex, proximityIndex);
+                mDistances[mOutputIndex] = mProximityInfoState.getNormalizedSquaredDistance(
+                        mInputIndex, proximityIndex);
             }
-            if (!isQuote(c)) {
+            if (!isSingleQuote(c)) {
                 incrementInputIndex();
                 incremented = true;
             }
@@ -362,7 +354,7 @@
         if (mExcessiveCount == 0 && mExcessivePos < mOutputIndex) {
             mExcessivePos = mOutputIndex;
         }
-        if (mExcessivePos < mInputLength - 1) {
+        if (mExcessivePos < mInputSize - 1) {
             mExceeding = mExcessivePos == mInputIndex && canTryCorrection;
         }
     }
@@ -370,7 +362,8 @@
     if (mSkipPos >= 0) {
         if (mSkippedCount == 0 && mSkipPos < mOutputIndex) {
             if (DEBUG_DICT) {
-                assert(mSkipPos == mOutputIndex - 1);
+                // TODO: Enable this assertion.
+                //assert(mSkipPos == mOutputIndex - 1);
             }
             mSkipPos = mOutputIndex;
         }
@@ -381,14 +374,15 @@
         if (mTransposedCount == 0 && mTransposedPos < mOutputIndex) {
             mTransposedPos = mOutputIndex;
         }
-        if (mTransposedPos < mInputLength - 1) {
+        if (mTransposedPos < mInputSize - 1) {
             mTransposing = mInputIndex == mTransposedPos && canTryCorrection;
         }
     }
 
     bool secondTransposing = false;
     if (mTransposedCount % 2 == 1) {
-        if (isEquivalentChar(mProximityInfo->getMatchedProximityId(mInputIndex - 1, c, false))) {
+        if (isEquivalentChar(mProximityInfoState.getMatchedProximityId(
+                mInputIndex - 1, c, false))) {
             ++mTransposedCount;
             secondTransposing = true;
         } else if (mCorrectionStates[mOutputIndex].mExceeding) {
@@ -399,7 +393,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);
@@ -417,20 +411,20 @@
             ? (noCorrectionsHappenedSoFar || mProximityCount == 0)
             : (noCorrectionsHappenedSoFar && mProximityCount == 0);
 
-    ProximityInfo::ProximityType matchedProximityCharId = secondTransposing
-            ? ProximityInfo::EQUIVALENT_CHAR
-            : mProximityInfo->getMatchedProximityId(
+    ProximityType matchedProximityCharId = secondTransposing
+            ? EQUIVALENT_CHAR
+            : mProximityInfoState.getMatchedProximityId(
                     mInputIndex, c, checkProximityChars, &proximityIndex);
 
-    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId
-            || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+    if (UNRELATED_CHAR == matchedProximityCharId
+            || ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
         if (canTryCorrection && mOutputIndex > 0
                 && mCorrectionStates[mOutputIndex].mProximityMatching
                 && mCorrectionStates[mOutputIndex].mExceeding
-                && isEquivalentChar(mProximityInfo->getMatchedProximityId(
+                && isEquivalentChar(mProximityInfoState.getMatchedProximityId(
                         mInputIndex, mWord[mOutputIndex - 1], false))) {
             if (DEBUG_CORRECTION
-                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (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]);
@@ -446,27 +440,27 @@
             // Here, we are doing something equivalent to matchedProximityCharId,
             // but we already know that "excessive char correction" just happened
             // so that we just need to check "mProximityCount == 0".
-            matchedProximityCharId = mProximityInfo->getMatchedProximityId(
+            matchedProximityCharId = mProximityInfoState.getMatchedProximityId(
                     mInputIndex, c, mProximityCount == 0, &proximityIndex);
         }
     }
 
-    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId
-            || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
-        if (ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+    if (UNRELATED_CHAR == matchedProximityCharId
+            || ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+        if (ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
             mAdditionalProximityMatching = true;
         }
         // TODO: Optimize
         // 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(mProximityInfo->getMatchedProximityId(
+                && isEquivalentChar(mProximityInfoState.getMatchedProximityId(
                         mInputIndex, mWord[mOutputIndex - 1], false))
                 && isEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex + 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex + 1, c, false))) {
             // Conversion t->e
             // Example:
             // occaisional -> occa   sional
@@ -478,7 +472,7 @@
                 && !mCorrectionStates[mOutputIndex].mTransposing
                 && mCorrectionStates[mOutputIndex - 1].mTransposing
                 && isEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex - 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex - 1, c, false))) {
             // Conversion t->s
             // Example:
             // chcolate -> chocolate
@@ -490,28 +484,28 @@
                 && mCorrectionStates[mOutputIndex].mProximityMatching
                 && mCorrectionStates[mOutputIndex].mSkipping
                 && isEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex - 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex - 1, c, false))) {
             // Conversion p->s
             // Note: This logic tries saving cases like contrst --> contrast -- "a" is one of
             // proximity chars of "s", but it should rather be handled as a skipped char.
             ++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
                 && isProximityCharOrEquivalentChar(
-                        mProximityInfo->getMatchedProximityId(mInputIndex + 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex + 1, c, false))) {
             // Conversion s->a
             incrementInputIndex();
             --mSkippedCount;
             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(
-                        mProximityInfo->getMatchedProximityId(mInputIndex + 1, c, false))) {
+                        mProximityInfoState.getMatchedProximityId(mInputIndex + 1, c, false))) {
             // 1.2. Excessive or transpose correction
             if (mTransposing) {
                 ++mTransposedCount;
@@ -520,7 +514,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);
@@ -536,20 +530,20 @@
             // 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,
                         mTransposedCount, mExcessiveCount, c);
             }
             return processSkipChar(c, isTerminal, false);
-        } else if (ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+        } else if (ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
             // As a last resort, use additional proximity characters
             mProximityMatching = true;
             ++mProximityCount;
             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,
@@ -557,7 +551,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);
@@ -567,20 +561,20 @@
             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)) {
         mMatching = true;
         ++mEquivalentCharCount;
-        mDistances[mOutputIndex] = mProximityInfo->getNormalizedSquaredDistance(mInputIndex, 0);
-    } else if (ProximityInfo::NEAR_PROXIMITY_CHAR == matchedProximityCharId) {
+        mDistances[mOutputIndex] = mProximityInfoState.getNormalizedSquaredDistance(mInputIndex, 0);
+    } else if (NEAR_PROXIMITY_CHAR == matchedProximityCharId) {
         mProximityMatching = true;
         ++mProximityCount;
         mDistances[mOutputIndex] =
-                mProximityInfo->getNormalizedSquaredDistance(mInputIndex, proximityIndex);
+                mProximityInfoState.getNormalizedSquaredDistance(mInputIndex, proximityIndex);
         if (DEBUG_CORRECTION
-                && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                && (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,
@@ -592,8 +586,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;
     }
@@ -604,7 +598,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.
@@ -620,7 +614,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,
@@ -634,13 +628,10 @@
     }
 }
 
-Correction::~Correction() {
-}
-
-inline static int getQuoteCount(const unsigned short* word, const int length) {
+inline static int getQuoteCount(const unsigned short *word, const int length) {
     int quoteCount = 0;
     for (int i = 0; i < length; ++i) {
-        if(word[i] == '\'') {
+        if (word[i] == SINGLE_QUOTE) {
             ++quoteCount;
         }
     }
@@ -657,12 +648,12 @@
 
 /* static */
 int Correction::RankingAlgorithm::calculateFinalProbability(const int inputIndex,
-        const int outputIndex, const int freq, int* editDistanceTable, const Correction* correction,
-        const int inputLength) {
+        const int outputIndex, const int freq, int *editDistanceTable, const Correction *correction,
+        const int inputSize) {
     const int excessivePos = correction->getExcessivePos();
     const int typedLetterMultiplier = correction->TYPED_LETTER_MULTIPLIER;
     const int fullWordMultiplier = correction->FULL_WORD_MULTIPLIER;
-    const ProximityInfo *proximityInfo = correction->mProximityInfo;
+    const ProximityInfoState *proximityInfoState = &correction->mProximityInfoState;
     const int skippedCount = correction->mSkippedCount;
     const int transposedCount = correction->mTransposedCount / 2;
     const int excessiveCount = correction->mExcessiveCount + correction->mTransposedCount % 2;
@@ -670,55 +661,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 unsigned short *word = correction->mWord;
     const bool skipped = skippedCount > 0;
 
     const int quoteDiffCount = max(0, getQuoteCount(word, outputLength)
-            - getQuoteCount(proximityInfo->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
@@ -737,8 +728,7 @@
         multiplyIntCapped(matchWeight, &finalFreq);
     }
 
-    if (proximityInfo->getMatchedProximityId(0, word[0], true)
-            == ProximityInfo::UNRELATED_CHAR) {
+    if (proximityInfoState->getMatchedProximityId(0, word[0], true) == UNRELATED_CHAR) {
         multiplyRate(FIRST_CHAR_DIFFERENT_DEMOTION_RATE, &finalFreq);
     }
 
@@ -748,8 +738,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);
@@ -764,7 +754,7 @@
     // Demotion for a word with excessive character
     if (excessiveCount > 0) {
         multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_DEMOTION_RATE, &finalFreq);
-        if (!lastCharExceeded && !proximityInfo->existsAdjacentProximityChars(excessivePos)) {
+        if (!lastCharExceeded && !proximityInfoState->existsAdjacentProximityChars(excessivePos)) {
             if (DEBUG_DICT_FULL) {
                 AKLOGI("Double excessive demotion");
             }
@@ -775,8 +765,9 @@
     }
 
     const bool performTouchPositionCorrection =
-            CALIBRATE_SCORE_BY_TOUCH_COORDINATES && proximityInfo->touchPositionCorrectionEnabled()
-                        && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0;
+            CALIBRATE_SCORE_BY_TOUCH_COORDINATES
+                    && proximityInfoState->touchPositionCorrectionEnabled()
+                    && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0;
     // Score calibration by touch coordinates is being done only for pure-fat finger typing error
     // cases.
     int additionalProximityCount = 0;
@@ -795,8 +786,8 @@
                 static const float MIN = 0.3f;
                 static const float R1 = NEUTRAL_SCORE_SQUARED_RADIUS;
                 static const float R2 = HALF_SCORE_SQUARED_RADIUS;
-                const float x = (float)squaredDistance
-                        / ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
+                const float x = static_cast<float>(squaredDistance)
+                        / ProximityInfoState::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
                 const float factor = max((x < R1)
                     ? (A * (R1 - x) + B * x) / R1
                     : (B * (R2 - x) + C * (x - R1)) / (R2 - R1), MIN);
@@ -850,7 +841,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) {
@@ -885,7 +876,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);
     }
@@ -895,8 +886,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;
     }
@@ -906,8 +897,8 @@
     }
 
     if (DEBUG_CORRECTION_FREQ
-            && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputLength)) {
-        DUMP_WORD(proximityInfo->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,
@@ -920,7 +911,7 @@
 /* static */
 int Correction::RankingAlgorithm::calcFreqForSplitMultipleWords(
         const int *freqArray, const int *wordLengthArray, const int wordCount,
-        const Correction* correction, const bool isSpaceProximity, const unsigned short *word) {
+        const Correction *correction, const bool isSpaceProximity, const unsigned short *word) {
     const int typedLetterMultiplier = correction->TYPED_LETTER_MULTIPLIER;
 
     bool firstCapitalizedWordDemotion = false;
@@ -946,7 +937,7 @@
 
     int totalLength = 0;
     int totalFreq = 0;
-    for (int i = 0; i < wordCount; ++i){
+    for (int i = 0; i < wordCount; ++i) {
         const int wordLength = wordLengthArray[i];
         if (wordLength <= 0) {
             return 0;
@@ -1050,10 +1041,10 @@
 
 /* Damerau-Levenshtein distance */
 inline static int editDistanceInternal(
-        int* editDistanceTable, const unsigned short* before,
-        const int beforeLength, const unsigned short* after, const int afterLength) {
+        int *editDistanceTable, const unsigned short *before,
+        const int beforeLength, const unsigned short *after, const int afterLength) {
     // dp[li][lo] dp[a][b] = dp[ a * lo + b]
-    int* dp = editDistanceTable;
+    int *dp = editDistanceTable;
     const int li = beforeLength + 1;
     const int lo = afterLength + 1;
     for (int i = 0; i < li; ++i) {
@@ -1089,8 +1080,8 @@
     return dp[li * lo - 1];
 }
 
-int Correction::RankingAlgorithm::editDistance(const unsigned short* before,
-        const int beforeLength, const unsigned short* after, const int afterLength) {
+int Correction::RankingAlgorithm::editDistance(const unsigned short *before,
+        const int beforeLength, const unsigned short *after, const int afterLength) {
     int table[(beforeLength + 1) * (afterLength + 1)];
     return editDistanceInternal(table, before, beforeLength, after, afterLength);
 }
@@ -1099,7 +1090,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].)
@@ -1111,15 +1102,15 @@
 //       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,
-        const int beforeLength, const unsigned short* after, const int afterLength,
+float Correction::RankingAlgorithm::calcNormalizedScore(const unsigned short *before,
+        const int beforeLength, const unsigned short *after, const int afterLength,
         const int score) {
     if (0 == beforeLength || 0 == afterLength) {
         return 0;
@@ -1136,15 +1127,16 @@
         return 0;
     }
 
-    const float maxScore = score >= S_INT_MAX ? S_INT_MAX : MAX_INITIAL_SCORE
-            * pow((float)TYPED_LETTER_MULTIPLIER,
-                    (float)min(beforeLength, afterLength - spaceCount)) * FULL_WORD_MULTIPLIER;
+    const float maxScore = score >= S_INT_MAX ? static_cast<float>(S_INT_MAX)
+            : static_cast<float>(MAX_INITIAL_SCORE)
+                    * powf(static_cast<float>(TYPED_LETTER_MULTIPLIER),
+                            static_cast<float>(min(beforeLength, afterLength - spaceCount)))
+                    * static_cast<float>(FULL_WORD_MULTIPLIER);
 
     // add a weight based on edit distance.
     // distance <= max(afterLength, beforeLength) == afterLength,
     // so, 0 <= distance / afterLength <= 1
-    const float weight = 1.0 - (float) distance / afterLength;
-    return (score / maxScore) * weight;
+    const float weight = 1.0f - static_cast<float>(distance) / static_cast<float>(afterLength);
+    return (static_cast<float>(score) / maxScore) * weight;
 }
-
 } // namespace latinime
diff --git a/native/jni/src/correction.h b/native/jni/src/correction.h
index 3300a84..f016d54 100644
--- a/native/jni/src/correction.h
+++ b/native/jni/src/correction.h
@@ -17,11 +17,13 @@
 #ifndef LATINIME_CORRECTION_H
 #define LATINIME_CORRECTION_H
 
-#include <assert.h>
+#include <cassert>
+#include <cstring> // for memset()
 #include <stdint.h>
-#include "correction_state.h"
 
+#include "correction_state.h"
 #include "defines.h"
+#include "proximity_info_state.h"
 
 namespace latinime {
 
@@ -37,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);
@@ -93,112 +193,45 @@
         }
     }
 
-    Correction(const int typedLetterMultiplier, const int fullWordMultiplier);
-    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;
-        static const int TYPED_LETTER_MULTIPLIER = 2;
-        static const int FULL_WORD_MULTIPLIER = 2;
-    };
-
- private:
     inline void incrementInputIndex();
     inline void incrementOutputIndex();
     inline void startToTraverseAllNodes();
-    inline bool isQuote(const unsigned short c);
+    inline bool isSingleQuote(const unsigned short c);
     inline CorrectionType processSkipChar(
             const int32_t c, const bool isTerminal, const bool inputIndexIncremented);
     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);
 
-    const int TYPED_LETTER_MULTIPLIER;
-    const int FULL_WORD_MULTIPLIER;
+    static const int TYPED_LETTER_MULTIPLIER = 2;
+    static const int FULL_WORD_MULTIPLIER = 2;
     const ProximityInfo *mProximityInfo;
 
     bool mUseFullEditDistance;
     bool mDoAutoCompletion;
     int mMaxEditDistance;
     int mMaxDepth;
-    int mInputLength;
+    int mInputSize;
     int mSpaceProximityPos;
     int mMissingSpacePos;
     int mTerminalInputIndex;
@@ -240,7 +273,7 @@
     bool mExceeding;
     bool mTransposing;
     bool mSkipping;
-
+    ProximityInfoState mProximityInfoState;
 };
 } // namespace latinime
 #endif // LATINIME_CORRECTION_H
diff --git a/native/jni/src/correction_state.h b/native/jni/src/correction_state.h
index 5b2cbd3..a63d4aa 100644
--- a/native/jni/src/correction_state.h
+++ b/native/jni/src/correction_state.h
@@ -79,6 +79,5 @@
     state->mSkipping = false;
     state->mAdditionalProximityMatching = false;
 }
-
 } // namespace latinime
 #endif // LATINIME_CORRECTION_STATE_H
diff --git a/native/jni/src/debug.h b/native/jni/src/debug.h
deleted file mode 100644
index 376ba59..0000000
--- a/native/jni/src/debug.h
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
-**
-** 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.
-*/
-
-#ifndef LATINIME_DEBUG_H
-#define LATINIME_DEBUG_H
-
-#include "defines.h"
-
-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)
-        output[i] = input[i] & 0xFF;
-    output[i] = 0;
-    return output;
-}
-
-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)
-        output[i] = input[i] & 0xFF;
-    if (i > 0) output[i-1] = c;
-    output[i] = 0;
-    return output;
-}
-
-static inline void LOGI_S16(unsigned short* string, const unsigned int length) {
-    unsigned char tmp_buffer[length];
-    convertToUnibyteString(string, tmp_buffer, length);
-    AKLOGI(">> %s", tmp_buffer);
-    // The log facility is throwing out log that comes too fast. The following
-    // is a dirty way of slowing down processing so that we can see all log.
-    // TODO : refactor this in a blocking log or something.
-    // usleep(10);
-}
-
-static inline void LOGI_S16_PLUS(unsigned short* string, const unsigned int length,
-        unsigned char c) {
-    unsigned char tmp_buffer[length+1];
-    convertToUnibyteStringAndReplaceLastChar(string, tmp_buffer, length, c);
-    AKLOGI(">> %s", tmp_buffer);
-    // Likewise
-    // usleep(10);
-}
-
-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));
-
-    buf[codesSize] = 0;
-    while (--codesSize >= 0)
-        buf[codesSize] = (unsigned char)codes[codesSize * MAX_PROXIMITY_CHARS];
-    AKLOGI("%s, WORD = %s", tag, buf);
-
-    free(buf);
-}
-
-#endif // LATINIME_DEBUG_H
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index cd2fc63..ad526fb 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -1,42 +1,86 @@
 /*
-**
-** Copyright 2010, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
+ * Copyright (C) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
 #ifndef LATINIME_DEFINES_H
 #define LATINIME_DEFINES_H
 
+#include <stdint.h>
+
 #if defined(FLAG_DO_PROFILE) || defined(FLAG_DBG)
-#include <cutils/log.h>
-#define AKLOGE ALOGE
-#define AKLOGI ALOGI
+#include <android/log.h>
+#ifndef LOG_TAG
+#define LOG_TAG "LatinIME: "
+#endif
+#define AKLOGE(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, fmt, ##__VA_ARGS__)
+#define AKLOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, fmt, ##__VA_ARGS__)
 
-#define DUMP_WORD(word, length) do { dumpWord(word, length); } while(0)
-#define DUMP_WORD_INT(word, length) do { dumpWordInt(word, length); } while(0)
+#define DUMP_RESULT(words, frequencies, maxWordCount, maxWordLength) do { \
+        dumpResult(words, frequencies, maxWordCount, maxWordLength); } while (0)
+#define DUMP_WORD(word, length) do { dumpWord(word, length); } while (0)
+#define DUMP_WORD_INT(word, length) do { dumpWordInt(word, length); } while (0)
+// TODO: INTS_TO_CHARS
+#define SHORTS_TO_CHARS(input, length, output) do { \
+        shortArrayToCharArray(input, length, output); } while (0)
 
-static inline void dumpWord(const unsigned short* word, const int length) {
+static inline void dumpWordInfo(const unsigned short *word, const int length,
+        const int rank, const int frequency) {
     static char charBuf[50];
-
-    for (int i = 0; i < length; ++i) {
-        charBuf[i] = word[i];
+    int i = 0;
+    for (; i < length; ++i) {
+        const unsigned short c = word[i];
+        if (c == 0) {
+            break;
+        }
+        // static_cast only for debugging
+        charBuf[i] = static_cast<char>(c);
     }
-    charBuf[length] = 0;
-    AKLOGI("[ %s ]", charBuf);
+    charBuf[i] = 0;
+    if (i > 1) {
+        AKLOGI("%2d [ %s ] (%d)", rank, charBuf, frequency);
+    }
 }
 
-static inline void dumpWordInt(const int* word, const int length) {
+static inline void dumpResult(
+        const unsigned short *outWords, const int *frequencies, const int maxWordCounts,
+        const int maxWordLength) {
+    AKLOGI("--- DUMP RESULT ---------");
+    for (int i = 0; i < maxWordCounts; ++i) {
+        dumpWordInfo(&outWords[i * maxWordLength], maxWordLength, i, frequencies[i]);
+    }
+    AKLOGI("-------------------------");
+}
+
+static inline void dumpWord(const unsigned short *word, const int length) {
+    static char charBuf[50];
+    int i = 0;
+    for (; i < length; ++i) {
+        const unsigned short c = word[i];
+        if (c == 0) {
+            break;
+        }
+        // static_cast only for debugging
+        charBuf[i] = static_cast<char>(c);
+    }
+    charBuf[i] = 0;
+    if (i > 1) {
+        AKLOGI("[ %s ]", charBuf);
+    }
+}
+
+static inline void dumpWordInt(const int *word, const int length) {
     static char charBuf[50];
 
     for (int i = 0; i < length; ++i) {
@@ -46,11 +90,58 @@
     AKLOGI("i[ %s ]", charBuf);
 }
 
+// TODO: Change this to intArrayToCharArray
+static inline void shortArrayToCharArray(
+        const unsigned short *input, const int length, char *output) {
+    int i = 0;
+    for (;i < length; ++i) {
+        const unsigned short c = input[i];
+        if (c == 0) {
+            break;
+        }
+        // static_cast only for debugging
+        output[i] = static_cast<char>(c);
+    }
+    output[i] = 0;
+}
+
+#ifndef __ANDROID__
+#include <cassert>
+#include <execinfo.h>
+#include <stdlib.h>
+
+#define ASSERT(success) do { if (!(success)) { showStackTrace(); assert(success);} } while (0)
+#define SHOW_STACK_TRACE do { showStackTrace(); } while (0)
+
+static inline void showStackTrace() {
+    void *callstack[128];
+    int i, frames = backtrace(callstack, 128);
+    char **strs = backtrace_symbols(callstack, frames);
+    for (i = 0; i < frames; ++i) {
+        if (i == 0) {
+            AKLOGI("=== Trace ===");
+            continue;
+        }
+        AKLOGI("%s", strs[i]);
+    }
+    free(strs);
+}
+#else
+#include <cassert>
+#define ASSERT(success) assert(success)
+#define SHOW_STACK_TRACE
+#endif
+
 #else
 #define AKLOGE(fmt, ...)
 #define AKLOGI(fmt, ...)
+#define DUMP_RESULT(words, frequencies, maxWordCount, maxWordLength)
 #define DUMP_WORD(word, length)
 #define DUMP_WORD_INT(word, length)
+#define ASSERT(success)
+#define SHOW_STACK_TRACE
+// TODO: INTS_TO_CHARS
+#define SHORTS_TO_CHARS(input, length, output)
 #endif
 
 #ifdef FLAG_DO_PROFILE
@@ -64,14 +155,14 @@
 
 #define PROF_RESET               prof_reset()
 #define PROF_COUNT(prof_buf_id)  ++profile_counter[prof_buf_id]
-#define PROF_OPEN                do { PROF_RESET; PROF_START(PROF_BUF_SIZE - 1); } while(0)
+#define PROF_OPEN                do { PROF_RESET; PROF_START(PROF_BUF_SIZE - 1); } while (0)
 #define PROF_START(prof_buf_id)  do { \
-        PROF_COUNT(prof_buf_id); profile_old[prof_buf_id] = (clock()); } while(0)
-#define PROF_CLOSE               do { PROF_END(PROF_BUF_SIZE - 1); PROF_OUTALL; } while(0)
+        PROF_COUNT(prof_buf_id); profile_old[prof_buf_id] = (clock()); } while (0)
+#define PROF_CLOSE               do { PROF_END(PROF_BUF_SIZE - 1); PROF_OUTALL; } while (0)
 #define PROF_END(prof_buf_id)    profile_buf[prof_buf_id] += ((clock()) - profile_old[prof_buf_id])
 #define PROF_CLOCKOUT(prof_buf_id) \
         AKLOGI("%s : clock is %f", __FUNCTION__, (clock() - profile_old[prof_buf_id]))
-#define PROF_OUTALL              do { AKLOGI("--- %s ---", __FUNCTION__); prof_out(); } while(0)
+#define PROF_OUTALL              do { AKLOGI("--- %s ---", __FUNCTION__); prof_out(); } while (0)
 
 static inline void prof_reset(void) {
     for (int i = 0; i < PROF_BUF_SIZE; ++i) {
@@ -86,17 +177,18 @@
         AKLOGI("Error: You must call PROF_OPEN before PROF_CLOSE.");
     }
     AKLOGI("Total time is %6.3f ms.",
-            profile_buf[PROF_BUF_SIZE - 1] * 1000 / (float)CLOCKS_PER_SEC);
+            profile_buf[PROF_BUF_SIZE - 1] * 1000.0f / static_cast<float>(CLOCKS_PER_SEC));
     float all = 0;
     for (int i = 0; i < PROF_BUF_SIZE - 1; ++i) {
         all += profile_buf[i];
     }
     if (all == 0) all = 1;
     for (int i = 0; i < PROF_BUF_SIZE - 1; ++i) {
-        if (profile_buf[i] != 0) {
+        if (profile_buf[i]) {
             AKLOGI("(%d): Used %4.2f%%, %8.4f ms. Called %d times.",
                     i, (profile_buf[i] * 100 / all),
-                    profile_buf[i] * 1000 / (float)CLOCKS_PER_SEC, profile_counter[i]);
+                    profile_buf[i] * 1000.0f / static_cast<float>(CLOCKS_PER_SEC),
+                    profile_counter[i]);
         }
     }
 }
@@ -116,10 +208,6 @@
 #endif // FLAG_DO_PROFILE
 
 #ifdef FLAG_DBG
-#include <cutils/log.h>
-#ifndef LOG_TAG
-#define LOG_TAG "LatinIME: "
-#endif
 #define DEBUG_DICT true
 #define DEBUG_DICT_FULL false
 #define DEBUG_EDIT_DISTANCE false
@@ -132,6 +220,12 @@
 #define DEBUG_CORRECTION_FREQ false
 #define DEBUG_WORDS_PRIORITY_QUEUE false
 
+#ifdef FLAG_FULL_DBG
+#define DEBUG_GEO_FULL true
+#else
+#define DEBUG_GEO_FULL false
+#endif
+
 #else // FLAG_DBG
 
 #define DEBUG_DICT false
@@ -146,6 +240,7 @@
 #define DEBUG_CORRECTION_FREQ false
 #define DEBUG_WORDS_PRIORITY_QUEUE false
 
+#define DEBUG_GEO_FULL false
 
 #endif // FLAG_DBG
 
@@ -176,15 +271,15 @@
 #define FLAG_BIGRAM_FREQ 0x7F
 
 #define DICTIONARY_VERSION_MIN 200
-#define NOT_VALID_WORD -99
-#define NOT_A_CHARACTER -1
-#define NOT_A_DISTANCE -1
-#define NOT_A_COORDINATE -1
-#define EQUIVALENT_CHAR_WITHOUT_DISTANCE_INFO -2
-#define PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO -3
-#define ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO -4
-#define NOT_AN_INDEX -1
-#define NOT_A_PROBABILITY -1
+#define NOT_VALID_WORD (-99)
+#define NOT_A_CODE_POINT (-1)
+#define NOT_A_DISTANCE (-1)
+#define NOT_A_COORDINATE (-1)
+#define EQUIVALENT_CHAR_WITHOUT_DISTANCE_INFO (-2)
+#define PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO (-3)
+#define ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO (-4)
+#define NOT_AN_INDEX (-1)
+#define NOT_A_PROBABILITY (-1)
 
 #define KEYCODE_SPACE ' '
 
@@ -225,9 +320,15 @@
 // This is only used for the size of array. Not to be used in c functions.
 #define MAX_WORD_LENGTH_INTERNAL 48
 
+// This must be the same as ProximityInfo#MAX_PROXIMITY_CHARS_SIZE, currently it's 16.
+#define MAX_PROXIMITY_CHARS_SIZE_INTERNAL 16
+
 // This must be equal to ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE in KeyDetector.java
 #define ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE 2
 
+// 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
@@ -252,12 +353,18 @@
 
 #define FIRST_WORD_INDEX 0
 
+#define MAX_SPACES_INTERNAL 16
+
+// Max Distance between point to key
+#define MAX_POINT_TO_KEY_LENGTH 10000000
+
+// The max number of the keys in one keyboard layout
+#define MAX_KEY_COUNT_IN_A_KEYBOARD 64
+
 // 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
 
@@ -286,7 +393,26 @@
 #define NEUTRAL_AREA_RADIUS_RATIO 1.3f
 
 // DEBUG
-#define INPUTLENGTH_FOR_DEBUG -1
-#define MIN_OUTPUT_INDEX_FOR_DEBUG -1
+#define INPUTLENGTH_FOR_DEBUG (-1)
+#define MIN_OUTPUT_INDEX_FOR_DEBUG (-1)
 
+#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
+  TypeName(const TypeName&);               \
+  void operator=(const TypeName&)
+
+#define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \
+  TypeName();                                    \
+  DISALLOW_COPY_AND_ASSIGN(TypeName)
+
+// Used as a return value for character comparison
+typedef enum {
+    // Same char, possibly with different case or accent
+    EQUIVALENT_CHAR,
+    // It is a char located nearby on the keyboard
+    NEAR_PROXIMITY_CHAR,
+    // It is an unrelated char
+    UNRELATED_CHAR,
+    // Additional proximity char which can differ by language.
+    ADDITIONAL_PROXIMITY_CHAR
+} ProximityType;
 #endif // LATINIME_DEFINES_H
diff --git a/native/jni/src/dic_traverse_wrapper.cpp b/native/jni/src/dic_traverse_wrapper.cpp
new file mode 100644
index 0000000..88ca9fa
--- /dev/null
+++ b/native/jni/src/dic_traverse_wrapper.cpp
@@ -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.
+ */
+
+#define LOG_TAG "LatinIME: jni: Session"
+
+#include "dic_traverse_wrapper.h"
+
+namespace latinime {
+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
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 1fb0247..2fbe83e 100644
--- a/native/jni/src/dictionary.cpp
+++ b/native/jni/src/dictionary.cpp
@@ -1,36 +1,44 @@
 /*
-**
-** Copyright 2009, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
-
-#include <stdio.h>
+ * Copyright (C) 2009, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
 #define LOG_TAG "LatinIME: dictionary.cpp"
 
+#include <stdint.h>
+
+#include "bigram_dictionary.h"
 #include "binary_format.h"
 #include "defines.h"
 #include "dictionary.h"
+#include "dic_traverse_wrapper.h"
+#include "gesture_decoder_wrapper.h"
+#include "unigram_dictionary.h"
 
 namespace latinime {
 
 // 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)
-    : 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",
@@ -38,30 +46,56 @@
             AKLOGI("IN NATIVE SUGGEST Version: %d", (mDict[0] & 0xFF));
         }
     }
-    mCorrection = new Correction(typedLetterMultiplier, fullWordMultiplier);
-    mWordsPriorityQueuePool = new WordsPriorityQueuePool(
-            maxWords, SUB_QUEUE_MAX_WORDS, maxWordLength);
-    const unsigned int headerSize = BinaryFormat::getHeaderSize(mDict);
-    const unsigned int options = BinaryFormat::getFlags(mDict);
-    mUnigramDictionary = new UnigramDictionary(mDict + headerSize, typedLetterMultiplier,
-            fullWordMultiplier, maxWordLength, maxWords, options);
-    mBigramDictionary = new BigramDictionary(mDict + headerSize, maxWordLength, this);
 }
 
 Dictionary::~Dictionary() {
-    delete mCorrection;
-    delete mWordsPriorityQueuePool;
     delete mUnigramDictionary;
     delete mBigramDictionary;
+    delete mGestureDecoder;
 }
 
-int Dictionary::getFrequency(const int32_t *word, int length) {
+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) const {
+    int result = 0;
+    if (isGesture) {
+        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);
+        }
+        return result;
+    } else {
+        std::map<int, int> bigramMap;
+        uint8_t bigramFilter[BIGRAM_FILTER_BYTE_SIZE];
+        mBigramDictionary->fillBigramAddressToFrequencyMapAndFilter(prevWordChars,
+                prevWordLength, &bigramMap, bigramFilter);
+        result = mUnigramDictionary->getSuggestions(proximityInfo, xcoordinates,
+                ycoordinates, codes, codesSize, &bigramMap, bigramFilter,
+                useFullEditDistance, outWords, frequencies, outputTypes);
+        return result;
+    }
+}
+
+int Dictionary::getBigrams(const int32_t *word, int length, int *codes, int codesSize,
+        unsigned short *outWords, int *frequencies, int *outputTypes) const {
+    if (length <= 0) return 0;
+    return mBigramDictionary->getBigrams(word, length, codes, codesSize, outWords, frequencies,
+            outputTypes);
+}
+
+int Dictionary::getFrequency(const int32_t *word, int length) const {
     return mUnigramDictionary->getFrequency(word, length);
 }
 
 bool Dictionary::isValidBigram(const int32_t *word1, int length1, const int32_t *word2,
-        int length2) {
+        int length2) const {
     return mBigramDictionary->isValidBigram(word1, length1, word2, length2);
 }
-
 } // namespace latinime
diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h
index 9f23679..a135889 100644
--- a/native/jni/src/dictionary.h
+++ b/native/jni/src/dictionary.h
@@ -17,55 +17,63 @@
 #ifndef LATINIME_DICTIONARY_H
 #define LATINIME_DICTIONARY_H
 
-#include <map>
+#include <stdint.h>
 
-#include "bigram_dictionary.h"
-#include "char_utils.h"
-#include "correction.h"
 #include "defines.h"
-#include "proximity_info.h"
-#include "unigram_dictionary.h"
-#include "words_priority_queue_pool.h"
 
 namespace latinime {
 
+class BigramDictionary;
+class IncrementalDecoderInterface;
+class ProximityInfo;
+class UnigramDictionary;
+
 class Dictionary {
  public:
-    Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust, int typedLetterMultipler,
-            int fullWordMultiplier, int maxWordLength, int maxWords);
+    // Taken from SuggestedWords.java
+    const static int KIND_TYPED = 0; // What user typed
+    const static int KIND_CORRECTION = 1; // Simple correction/suggestion
+    const static int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
+    const static int KIND_WHITELIST = 3; // Whitelisted word
+    const static int KIND_BLACKLIST = 4; // Blacklisted word
+    const static int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
+    const static int KIND_APP_DEFINED = 6; // Suggested by the application
+    const static int KIND_SHORTCUT = 7; // A shortcut
+    const static int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
 
-    int getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates, int *ycoordinates,
-            int *codes, int codesSize, const int32_t* prevWordChars, const int prevWordLength,
-            bool useFullEditDistance, unsigned short *outWords, int *frequencies) {
-        std::map<int, int> bigramMap;
-        uint8_t bigramFilter[BIGRAM_FILTER_BYTE_SIZE];
-        mBigramDictionary->fillBigramAddressToFrequencyMapAndFilter(prevWordChars,
-                prevWordLength, &bigramMap, bigramFilter);
-        return mUnigramDictionary->getSuggestions(proximityInfo, mWordsPriorityQueuePool,
-                mCorrection, xcoordinates, ycoordinates, codes, codesSize, &bigramMap,
-                bigramFilter, useFullEditDistance, outWords, frequencies);
-    }
+    Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust, int typedLetterMultipler,
+            int fullWordMultiplier, int maxWordLength, int maxWords, int maxPredictions);
+
+    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) const;
 
     int getBigrams(const int32_t *word, int length, int *codes, int codesSize,
-            unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams) {
-        return mBigramDictionary->getBigrams(word, length, codes, codesSize, outWords, frequencies,
-                maxWordLength, maxBigrams);
-    }
+            unsigned short *outWords, int *frequencies, int *outputTypes) const;
 
-    int getFrequency(const int32_t *word, int length);
-    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2);
-    void *getDict() { return (void *)mDict; }
-    int getDictSize() { return mDictSize; }
-    int getMmapFd() { return mMmapFd; }
-    int getDictBufAdjust() { return mDictBufAdjust; }
-    ~Dictionary();
+    int getFrequency(const int32_t *word, int length) const;
+    bool isValidBigram(const int32_t *word1, int length1, const int32_t *word2, int length2) const;
+    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; }
+    virtual ~Dictionary();
 
     // public static utility methods
     // static inline methods should be defined in the header file
     static int wideStrLen(unsigned short *str);
 
  private:
-    const unsigned char *mDict;
+    DISALLOW_IMPLICIT_CONSTRUCTORS(Dictionary);
+    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.
@@ -73,21 +81,21 @@
     const int mMmapFd;
     const int mDictBufAdjust;
 
-    UnigramDictionary *mUnigramDictionary;
-    BigramDictionary *mBigramDictionary;
-    WordsPriorityQueuePool *mWordsPriorityQueuePool;
-    Correction *mCorrection;
+    const UnigramDictionary *mUnigramDictionary;
+    const BigramDictionary *mBigramDictionary;
+    IncrementalDecoderInterface *mGestureDecoder;
 };
 
 // public static utility methods
 // static inline methods should be defined in the header file
 inline int Dictionary::wideStrLen(unsigned short *str) {
     if (!str) return 0;
-    unsigned short *end = str;
-    while (*end)
-        end++;
-    return end - str;
+    int length = 0;
+    while (*str) {
+        str++;
+        length++;
+    }
+    return length;
 }
 } // namespace latinime
-
 #endif // LATINIME_DICTIONARY_H
diff --git a/native/jni/src/geometry_utils.h b/native/jni/src/geometry_utils.h
new file mode 100644
index 0000000..3892b46
--- /dev/null
+++ b/native/jni/src/geometry_utils.h
@@ -0,0 +1,93 @@
+/*
+ * 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
+
+#define ROUND_FLOAT_10000(f) ((f) < 1000.0f && (f) > 0.001f) \
+        ? (floorf((f) * 10000.0f) / 10000.0f) : (f)
+
+#define SQUARE_FLOAT(x) ((x) * (x))
+
+namespace latinime {
+
+static inline float getSquaredDistanceFloat(float x1, float y1, float x2, float y2) {
+    const float deltaX = x1 - x2;
+    const float deltaY = y1 - y2;
+    return SQUARE_FLOAT(deltaX) + SQUARE_FLOAT(deltaY);
+}
+
+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 deltaA = fabsf(a1 - a2);
+    const float diff = ROUND_FLOAT_10000(deltaA);
+    if (diff > M_PI_F) {
+        const float normalizedDiff = 2.0f * M_PI_F - diff;
+        return ROUND_FLOAT_10000(normalizedDiff);
+    }
+    return diff;
+}
+
+static inline float pointToLineSegSquaredDistanceFloat(
+        float x, float y, float x1, float y1, float x2, float y2, bool extend) {
+    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 = SQUARE_FLOAT(ray2x) + SQUARE_FLOAT(ray2y);
+    const float projectionLengthSqr = dotProduct / lineLengthSqr;
+
+    float projectionX;
+    float projectionY;
+    if (!extend && projectionLengthSqr < 0.0f) {
+        projectionX = x1;
+        projectionY = y1;
+    } else if (!extend && 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.cpp b/native/jni/src/gesture/gesture_decoder_wrapper.cpp
new file mode 100644
index 0000000..afbe0c5
--- /dev/null
+++ b/native/jni/src/gesture/gesture_decoder_wrapper.cpp
@@ -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.
+ */
+
+#include "gesture_decoder_wrapper.h"
+
+namespace latinime {
+    IncrementalDecoderInterface *
+            (*GestureDecoderWrapper::sGestureDecoderFactoryMethod)(int, int) = 0;
+} // namespace latinime
diff --git a/native/jni/src/gesture/gesture_decoder_wrapper.h b/native/jni/src/gesture/gesture_decoder_wrapper.h
new file mode 100644
index 0000000..92e1ded
--- /dev/null
+++ b/native/jni/src/gesture/gesture_decoder_wrapper.h
@@ -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.
+ */
+
+#ifndef LATINIME_GESTURE_DECODER_WRAPPER_H
+#define LATINIME_GESTURE_DECODER_WRAPPER_H
+
+#include <stdint.h>
+#include "defines.h"
+#include "incremental_decoder_interface.h"
+
+namespace latinime {
+
+class UnigramDictionary;
+class BigramDictionary;
+class ProximityInfo;
+
+class GestureDecoderWrapper : public IncrementalDecoderInterface {
+ public:
+    GestureDecoderWrapper(const int maxWordLength, const int maxWords)
+            : mIncrementalDecoderInterface(getGestureDecoderInstance(maxWordLength, maxWords)) {
+    }
+
+    virtual ~GestureDecoderWrapper() {
+        delete mIncrementalDecoderInterface;
+    }
+
+    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, traverseSession, inputXs, inputYs, times, pointerIds, codes,
+                inputSize, commitPoint, outWords, frequencies, outputIndices, outputTypes);
+    }
+
+    static void setGestureDecoderFactoryMethod(
+            IncrementalDecoderInterface *(*factoryMethod)(int, int)) {
+        sGestureDecoderFactoryMethod = factoryMethod;
+    }
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(GestureDecoderWrapper);
+    static IncrementalDecoderInterface *getGestureDecoderInstance(int maxWordLength, int maxWords) {
+        if (sGestureDecoderFactoryMethod) {
+            return sGestureDecoderFactoryMethod(maxWordLength, maxWords);
+        }
+        return 0;
+    }
+
+    static IncrementalDecoderInterface *(*sGestureDecoderFactoryMethod)(int, int);
+    IncrementalDecoderInterface *mIncrementalDecoderInterface;
+};
+} // namespace latinime
+#endif // LATINIME_GESTURE_DECODER_WRAPPER_H
diff --git a/native/jni/src/gesture/incremental_decoder_interface.h b/native/jni/src/gesture/incremental_decoder_interface.h
new file mode 100644
index 0000000..d1395aa
--- /dev/null
+++ b/native/jni/src/gesture/incremental_decoder_interface.h
@@ -0,0 +1,41 @@
+/*
+ * 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_INCREMENTAL_DECODER_INTERFACE_H
+#define LATINIME_INCREMENTAL_DECODER_INTERFACE_H
+
+#include <stdint.h>
+#include "defines.h"
+
+namespace latinime {
+
+class UnigramDictionary;
+class BigramDictionary;
+class ProximityInfo;
+
+class IncrementalDecoderInterface {
+ public:
+    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.cpp b/native/jni/src/gesture/incremental_decoder_wrapper.cpp
new file mode 100644
index 0000000..8fcda6c
--- /dev/null
+++ b/native/jni/src/gesture/incremental_decoder_wrapper.cpp
@@ -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.
+ */
+
+#include "incremental_decoder_wrapper.h"
+
+namespace latinime {
+    IncrementalDecoderInterface *
+            (*IncrementalDecoderWrapper::sIncrementalDecoderFactoryMethod)(int, int) = 0;
+} // namespace latinime
diff --git a/native/jni/src/gesture/incremental_decoder_wrapper.h b/native/jni/src/gesture/incremental_decoder_wrapper.h
new file mode 100644
index 0000000..da7afdb
--- /dev/null
+++ b/native/jni/src/gesture/incremental_decoder_wrapper.h
@@ -0,0 +1,71 @@
+/*
+ * 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_INCREMENTAL_DECODER_WRAPPER_H
+#define LATINIME_INCREMENTAL_DECODER_WRAPPER_H
+
+#include <stdint.h>
+#include "defines.h"
+#include "incremental_decoder_interface.h"
+
+namespace latinime {
+
+class UnigramDictionary;
+class BigramDictionary;
+class ProximityInfo;
+
+class IncrementalDecoderWrapper : public IncrementalDecoderInterface {
+ public:
+    IncrementalDecoderWrapper(const int maxWordLength, const int maxWords)
+            : mIncrementalDecoderInterface(getIncrementalDecoderInstance(maxWordLength, maxWords)) {
+    }
+
+    virtual ~IncrementalDecoderWrapper() {
+        delete mIncrementalDecoderInterface;
+    }
+
+    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, traverseSession, inputXs, inputYs, times, pointerIds, codes,
+                inputSize, commitPoint, outWords, frequencies, outputIndices, outputTypes);
+    }
+
+    static void setIncrementalDecoderFactoryMethod(
+            IncrementalDecoderInterface *(*factoryMethod)(int, int)) {
+        sIncrementalDecoderFactoryMethod = factoryMethod;
+    }
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(IncrementalDecoderWrapper);
+    static IncrementalDecoderInterface *getIncrementalDecoderInstance(int maxWordLength,
+            int maxWords) {
+        if (sIncrementalDecoderFactoryMethod) {
+            return sIncrementalDecoderFactoryMethod(maxWordLength, maxWords);
+        }
+        return 0;
+    }
+
+    static IncrementalDecoderInterface *(*sIncrementalDecoderFactoryMethod)(int, int);
+    IncrementalDecoderInterface *mIncrementalDecoderInterface;
+};
+} // namespace latinime
+#endif // LATINIME_INCREMENTAL_DECODER_WRAPPER_H
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 80e14fb..c9f83b6 100644
--- a/native/jni/src/proximity_info.cpp
+++ b/native/jni/src/proximity_info.cpp
@@ -14,93 +14,86 @@
  * limitations under the License.
  */
 
-#include <assert.h>
-#include <stdio.h>
-#include <string>
+#include <cassert>
+#include <cmath>
+#include <cstring>
 
 #define LOG_TAG "LatinIME: proximity_info.cpp"
 
 #include "additional_proximity_chars.h"
+#include "char_utils.h"
 #include "defines.h"
-#include "dictionary.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 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));
     }
 }
 
-const float ProximityInfo::NOT_A_DISTANCE_FLOAT = -1.0f;
+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(const std::string localeStr, const int maxProximityCharsSize,
+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),
-          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),
           KEY_COUNT(min(keyCount, MAX_KEY_COUNT_IN_A_KEYBOARD)),
+          KEYBOARD_WIDTH(keyboardWidth), KEYBOARD_HEIGHT(keyboardHeight),
           HAS_TOUCH_POSITION_CORRECTION_DATA(keyCount > 0 && keyXCoordinates && keyYCoordinates
                   && keyWidths && keyHeights && keyCharCodes && sweetSpotCenterXs
                   && sweetSpotCenterYs && sweetSpotRadii),
-          mLocaleStr(localeStr),
-          mInputXCoordinates(0), mInputYCoordinates(0),
-          mTouchPositionCorrectionEnabled(false) {
+          mProximityCharsArray(new int32_t[GRID_WIDTH * GRID_HEIGHT * MAX_PROXIMITY_CHARS_SIZE
+                  /* proximityGridLength */]),
+          mCodeToKeyMap() {
     const int proximityGridLength = GRID_WIDTH * GRID_HEIGHT * MAX_PROXIMITY_CHARS_SIZE;
-    mProximityCharsArray = new int32_t[proximityGridLength];
-    mInputCodes = new int32_t[MAX_PROXIMITY_CHARS_SIZE * MAX_WORD_LENGTH_INTERNAL];
     if (DEBUG_PROXIMITY_INFO) {
         AKLOGI("Create proximity info array %d", proximityGridLength);
     }
-    memcpy(mProximityCharsArray, proximityCharsArray,
-            proximityGridLength * sizeof(mProximityCharsArray[0]));
-    const int normalizedSquaredDistancesLength =
-            MAX_PROXIMITY_CHARS_SIZE * MAX_WORD_LENGTH_INTERNAL;
-    mNormalizedSquaredDistances = new int[normalizedSquaredDistancesLength];
-    for (int i = 0; i < normalizedSquaredDistancesLength; ++i) {
-        mNormalizedSquaredDistances[i] = NOT_A_DISTANCE;
+    const jsize localeCStrUtf8Length = env->GetStringUTFLength(localeJStr);
+    if (localeCStrUtf8Length >= MAX_LOCALE_STRING_LENGTH) {
+        AKLOGI("Locale string length too long: length=%d", localeCStrUtf8Length);
+        assert(false);
     }
-
-    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]));
-
-    initializeCodeToKeyIndex();
-}
-
-// Build the reversed look up table from the char code to the index in mKeyXCoordinates,
-// mKeyYCoordinates, mKeyWidths, mKeyHeights, mKeyCharCodes.
-void ProximityInfo::initializeCodeToKeyIndex() {
-    memset(mCodeToKeyIndex, -1, (MAX_CHAR_CODE + 1) * sizeof(mCodeToKeyIndex[0]));
-    for (int i = 0; i < KEY_COUNT; ++i) {
-        const int code = mKeyCharCodes[i];
-        if (0 <= code && code <= MAX_CHAR_CODE) {
-            mCodeToKeyIndex[code] = i;
-        }
-    }
+    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, mKeyCodePoints);
+    safeGetOrFillZeroFloatArrayRegion(env, sweetSpotCenterXs, KEY_COUNT, mSweetSpotCenterXs);
+    safeGetOrFillZeroFloatArrayRegion(env, sweetSpotCenterYs, KEY_COUNT, mSweetSpotCenterYs);
+    safeGetOrFillZeroFloatArrayRegion(env, sweetSpotRadii, KEY_COUNT, mSweetSpotRadii);
+    initializeG();
 }
 
 ProximityInfo::~ProximityInfo() {
-    delete[] mNormalizedSquaredDistances;
     delete[] mProximityCharsArray;
-    delete[] mInputCodes;
 }
 
 inline int ProximityInfo::getStartIndexFromCoordinates(const int x, const int y) const {
@@ -112,7 +105,8 @@
     if (x < 0 || y < 0) {
         if (DEBUG_DICT) {
             AKLOGI("HasSpaceProximity: Illegal coordinates (%d, %d)", x, y);
-            assert(false);
+            // TODO: Enable this assertion.
+            //assert(false);
         }
         return false;
     }
@@ -121,24 +115,33 @@
     if (DEBUG_PROXIMITY_INFO) {
         AKLOGI("hasSpaceProximity: index %d, %d, %d", startIndex, x, y);
     }
+    int32_t *proximityCharsArray = mProximityCharsArray;
     for (int i = 0; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
         if (DEBUG_PROXIMITY_INFO) {
             AKLOGI("Index: %d", mProximityCharsArray[startIndex + i]);
         }
-        if (mProximityCharsArray[startIndex + i] == KEYCODE_SPACE) {
+        if (proximityCharsArray[startIndex + i] == KEYCODE_SPACE) {
             return true;
         }
     }
     return false;
 }
 
-bool ProximityInfo::isOnKey(const int keyId, const int x, const int y) const {
-    if (keyId < 0) return true; // NOT_A_ID is -1, but return whenever < 0 just in case
-    const int left = mKeyXCoordinates[keyId];
-    const int top = mKeyYCoordinates[keyId];
-    const int right = left + mKeyWidths[keyId] + 1;
-    const int bottom = top + mKeyHeights[keyId];
-    return left < right && top < bottom && x >= left && x < right && y >= top && y < bottom;
+static inline float getNormalizedSquaredDistanceFloat(float x1, float y1, float x2, float y2,
+        float scale) {
+    const float deltaX = x1 - x2;
+    const float deltaY = y1 - y2;
+    return (SQUARE_FLOAT(deltaX) + SQUARE_FLOAT(deltaY)) / SQUARE_FLOAT(scale);
+}
+
+float ProximityInfo::getNormalizedSquaredDistanceFromCenterFloat(
+        const int keyId, const int x, const int y) const {
+    const float centerX = static_cast<float>(getKeyCenterXOfKeyIdG(keyId));
+    const float centerY = static_cast<float>(getKeyCenterYOfKeyIdG(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 {
@@ -156,16 +159,17 @@
 
 void ProximityInfo::calculateNearbyKeyCodes(
         const int x, const int y, const int32_t primaryKey, int *inputCodes) const {
+    int32_t *proximityCharsArray = mProximityCharsArray;
     int insertPos = 0;
     inputCodes[insertPos++] = primaryKey;
     const int startIndex = getStartIndexFromCoordinates(x, y);
     if (startIndex >= 0) {
         for (int i = 0; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
-            const int32_t c = mProximityCharsArray[startIndex + i];
+            const int32_t c = proximityCharsArray[startIndex + i];
             if (c < KEYCODE_SPACE || c == primaryKey) {
                 continue;
             }
-            const int keyIndex = getKeyIndex(c);
+            const int keyIndex = getKeyIndexOf(c);
             const bool onKey = isOnKey(keyIndex, x, y);
             const int distance = squaredDistanceToEdge(keyIndex, x, y);
             if (onKey || distance < MOST_COMMON_KEY_WIDTH_SQUARE) {
@@ -179,7 +183,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) {
@@ -189,13 +193,13 @@
                 return;
             }
 
-            const int32_t* additionalProximityChars =
-                    AdditionalProximityChars::getAdditionalChars(&mLocaleStr, primaryKey);
+            const int32_t *additionalProximityChars =
+                    AdditionalProximityChars::getAdditionalChars(mLocaleStr, primaryKey);
             for (int j = 0; j < additionalProximitySize; ++j) {
                 const int32_t ac = additionalProximityChars[j];
                 int k = 0;
                 for (; k < insertPos; ++k) {
-                    if ((int)ac == inputCodes[k]) {
+                    if (static_cast<int>(ac) == inputCodes[k]) {
                         break;
                     }
                 }
@@ -214,256 +218,78 @@
     }
     // Add a delimiter for the proximity characters
     for (int i = insertPos; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
-        inputCodes[i] = NOT_A_CODE;
+        inputCodes[i] = NOT_A_CODE_POINT;
     }
 }
 
-void ProximityInfo::setInputParams(const int32_t* inputCodes, const int inputLength,
-        const int* xCoordinates, const int* yCoordinates) {
-    memset(mInputCodes, 0,
-            MAX_WORD_LENGTH_INTERNAL * MAX_PROXIMITY_CHARS_SIZE * sizeof(mInputCodes[0]));
-
-    for (int i = 0; i < inputLength; ++i) {
-        const int32_t primaryKey = inputCodes[i];
-        const int x = xCoordinates[i];
-        const int y = yCoordinates[i];
-        int *proximities = &mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE];
-        calculateNearbyKeyCodes(x, y, primaryKey, proximities);
-    }
-
-    if (DEBUG_PROXIMITY_CHARS) {
-        for (int i = 0; i < inputLength; ++i) {
-            AKLOGI("---");
-            for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE; ++j) {
-                int icc = mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE + j];
-                int icfjc = inputCodes[i * MAX_PROXIMITY_CHARS_SIZE + j];
-                icc+= 0;
-                icfjc += 0;
-                AKLOGI("--- (%d)%c,%c", i, icc, icfjc);
-                AKLOGI("---             A<%d>,B<%d>", icc, icfjc);
-            }
-        }
-    }
-    //Keep for debug, sorry
-    //for (int i = 0; i < MAX_WORD_LENGTH_INTERNAL * MAX_PROXIMITY_CHARS_SIZE; ++i) {
-    //if (i < inputLength * MAX_PROXIMITY_CHARS_SIZE) {
-    //mInputCodes[i] = mInputCodesFromJava[i];
-    //} else {
-    // mInputCodes[i] = 0;
-    // }
-    //}
-    mInputXCoordinates = xCoordinates;
-    mInputYCoordinates = yCoordinates;
-    mTouchPositionCorrectionEnabled =
-            HAS_TOUCH_POSITION_CORRECTION_DATA && xCoordinates && yCoordinates;
-    mInputLength = inputLength;
-    for (int i = 0; i < inputLength; ++i) {
-        mPrimaryInputWord[i] = getPrimaryCharAt(i);
-    }
-    mPrimaryInputWord[inputLength] = 0;
-    if (DEBUG_PROXIMITY_CHARS) {
-        AKLOGI("--- setInputParams");
-    }
-    for (int i = 0; i < mInputLength; ++i) {
-        const int *proximityChars = getProximityCharsAt(i);
-        const int primaryKey = proximityChars[0];
-        const int x = xCoordinates[i];
-        const int y = yCoordinates[i];
-        if (DEBUG_PROXIMITY_CHARS) {
-            int a = x + y + primaryKey;
-            a += 0;
-            AKLOGI("--- Primary = %c, x = %d, y = %d", primaryKey, x, y);
-            // Keep debug code just in case
-            //int proximities[50];
-            //for (int m = 0; m < 50; ++m) {
-            //proximities[m] = 0;
-            //}
-            //calculateNearbyKeyCodes(x, y, primaryKey, proximities);
-            //for (int l = 0; l < 50 && proximities[l] > 0; ++l) {
-            //if (DEBUG_PROXIMITY_CHARS) {
-            //AKLOGI("--- native Proximity (%d) = %c", l, proximities[l]);
-            //}
-            //}
-        }
-        for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE && proximityChars[j] > 0; ++j) {
-            const int currentChar = proximityChars[j];
-            const float squaredDistance = hasInputCoordinates()
-                    ? calculateNormalizedSquaredDistance(getKeyIndex(currentChar), i)
-                    : NOT_A_DISTANCE_FLOAT;
-            if (squaredDistance >= 0.0f) {
-                mNormalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE + j] =
-                        (int)(squaredDistance * NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR);
-            } else {
-                mNormalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE + j] = (j == 0)
-                        ? EQUIVALENT_CHAR_WITHOUT_DISTANCE_INFO
-                        : PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO;
-            }
-            if (DEBUG_PROXIMITY_CHARS) {
-                AKLOGI("--- Proximity (%d) = %c", j, currentChar);
-            }
-        }
-    }
-}
-
-inline float square(const float x) { return x * x; }
-
-float ProximityInfo::calculateNormalizedSquaredDistance(
-        const int keyIndex, const int inputIndex) const {
-    if (keyIndex == NOT_AN_INDEX) {
-        return NOT_A_DISTANCE_FLOAT;
-    }
-    if (!hasSweetSpotData(keyIndex)) {
-        return NOT_A_DISTANCE_FLOAT;
-    }
-    if (NOT_A_COORDINATE == mInputXCoordinates[inputIndex]) {
-        return NOT_A_DISTANCE_FLOAT;
-    }
-    const float squaredDistance = calculateSquaredDistanceFromSweetSpotCenter(keyIndex, inputIndex);
-    const float squaredRadius = square(mSweetSpotRadii[keyIndex]);
-    return squaredDistance / squaredRadius;
-}
-
-bool ProximityInfo::hasInputCoordinates() const {
-    return mInputXCoordinates && mInputYCoordinates;
-}
-
-int ProximityInfo::getKeyIndex(const int c) const {
+int ProximityInfo::getKeyIndexOf(const int c) const {
     if (KEY_COUNT == 0) {
         // We do not have the coordinate data
         return NOT_AN_INDEX;
     }
-    const unsigned short baseLowerC = toBaseLowerCase(c);
-    if (baseLowerC > MAX_CHAR_CODE) {
-        return NOT_AN_INDEX;
+    const int baseLowerC = static_cast<int>(toBaseLowerCase(c));
+    hash_map_compat<int, int>::const_iterator mapPos = mCodeToKeyMap.find(baseLowerC);
+    if (mapPos != mCodeToKeyMap.end()) {
+        return mapPos->second;
     }
-    return mCodeToKeyIndex[baseLowerC];
+    return NOT_AN_INDEX;
 }
 
-float ProximityInfo::calculateSquaredDistanceFromSweetSpotCenter(
-        const int keyIndex, const int inputIndex) const {
-    const float sweetSpotCenterX = mSweetSpotCenterXs[keyIndex];
-    const float sweetSpotCenterY = mSweetSpotCenterYs[keyIndex];
-    const float inputX = (float)mInputXCoordinates[inputIndex];
-    const float inputY = (float)mInputYCoordinates[inputIndex];
-    return square(inputX - sweetSpotCenterX) + square(inputY - sweetSpotCenterY);
+int ProximityInfo::getCodePointOf(const int keyIndex) const {
+    if (keyIndex < 0 || keyIndex >= KEY_COUNT) {
+        return NOT_A_CODE_POINT;
+    }
+    return mKeyIndexToCodePointG[keyIndex];
 }
 
-inline const int* ProximityInfo::getProximityCharsAt(const int index) const {
-    return mInputCodes + (index * MAX_PROXIMITY_CHARS_SIZE);
-}
-
-unsigned short ProximityInfo::getPrimaryCharAt(const int index) const {
-    return getProximityCharsAt(index)[0];
-}
-
-inline bool ProximityInfo::existsCharInProximityAt(const int index, const int c) const {
-    const int *chars = getProximityCharsAt(index);
-    int i = 0;
-    while (chars[i] > 0 && i < MAX_PROXIMITY_CHARS_SIZE) {
-        if (chars[i++] == c) {
-            return true;
+void ProximityInfo::initializeG() {
+    // TODO: Optimize
+    for (int i = 0; i < KEY_COUNT; ++i) {
+        const int code = mKeyCodePoints[i];
+        const int lowerCode = toBaseLowerCase(code);
+        mCenterXsG[i] = mKeyXCoordinates[i] + mKeyWidths[i] / 2;
+        mCenterYsG[i] = mKeyYCoordinates[i] + mKeyHeights[i] / 2;
+        mCodeToKeyMap[lowerCode] = i;
+        mKeyIndexToCodePointG[i] = lowerCode;
+    }
+    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];
         }
     }
-    return false;
 }
 
-bool ProximityInfo::existsAdjacentProximityChars(const int index) const {
-    if (index < 0 || index >= mInputLength) return false;
-    const int currentChar = getPrimaryCharAt(index);
-    const int leftIndex = index - 1;
-    if (leftIndex >= 0 && existsCharInProximityAt(leftIndex, currentChar)) {
-        return true;
-    }
-    const int rightIndex = index + 1;
-    if (rightIndex < mInputLength && existsCharInProximityAt(rightIndex, currentChar)) {
-        return true;
-    }
-    return false;
+int ProximityInfo::getKeyCenterXOfCodePointG(int charCode) const {
+    return getKeyCenterXOfKeyIdG(getKeyIndexOf(charCode));
 }
 
-// In the following function, c is the current character of the dictionary word
-// currently examined.
-// currentChars is an array containing the keys close to the character the
-// user actually typed at the same position. We want to see if c is in it: if so,
-// then the word contains at that position a character close to what the user
-// typed.
-// What the user typed is actually the first character of the array.
-// proximityIndex is a pointer to the variable where getMatchedProximityId returns
-// the index of c in the proximity chars of the input index.
-// Notice : accented characters do not have a proximity list, so they are alone
-// in their list. The non-accented version of the character should be considered
-// "close", but not the other keys close to the non-accented version.
-ProximityInfo::ProximityType ProximityInfo::getMatchedProximityId(const int index,
-        const unsigned short c, const bool checkProximityChars, int *proximityIndex) const {
-    const int *currentChars = getProximityCharsAt(index);
-    const int firstChar = currentChars[0];
-    const unsigned short baseLowerC = toBaseLowerCase(c);
-
-    // The first char in the array is what user typed. If it matches right away,
-    // that means the user typed that same char for this pos.
-    if (firstChar == baseLowerC || firstChar == c) {
-        return EQUIVALENT_CHAR;
-    }
-
-    if (!checkProximityChars) return UNRELATED_CHAR;
-
-    // If the non-accented, lowercased version of that first character matches c,
-    // then we have a non-accented version of the accented character the user
-    // typed. Treat it as a close char.
-    if (toBaseLowerCase(firstChar) == baseLowerC)
-        return NEAR_PROXIMITY_CHAR;
-
-    // Not an exact nor an accent-alike match: search the list of close keys
-    int j = 1;
-    while (j < MAX_PROXIMITY_CHARS_SIZE
-            && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
-        const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
-        if (matched) {
-            if (proximityIndex) {
-                *proximityIndex = j;
-            }
-            return NEAR_PROXIMITY_CHAR;
-        }
-        ++j;
-    }
-    if (j < MAX_PROXIMITY_CHARS_SIZE
-            && currentChars[j] == ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
-        ++j;
-        while (j < MAX_PROXIMITY_CHARS_SIZE
-                && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
-            const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
-            if (matched) {
-                if (proximityIndex) {
-                    *proximityIndex = j;
-                }
-                return ADDITIONAL_PROXIMITY_CHAR;
-            }
-            ++j;
-        }
-    }
-
-    // Was not included, signal this as an unrelated character.
-    return UNRELATED_CHAR;
+int ProximityInfo::getKeyCenterYOfCodePointG(int charCode) const {
+    return getKeyCenterYOfKeyIdG(getKeyIndexOf(charCode));
 }
 
-bool ProximityInfo::sameAsTyped(const unsigned short *word, int length) const {
-    if (length != mInputLength) {
-        return false;
+int ProximityInfo::getKeyCenterXOfKeyIdG(int keyId) const {
+    if (keyId >= 0) {
+        return mCenterXsG[keyId];
     }
-    const int *inputCodes = mInputCodes;
-    while (length--) {
-        if ((unsigned int) *inputCodes != (unsigned int) *word) {
-            return false;
-        }
-        inputCodes += MAX_PROXIMITY_CHARS_SIZE;
-        word++;
-    }
-    return true;
+    return 0;
 }
 
-const int ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2;
-const int ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
-const int ProximityInfo::MAX_KEY_COUNT_IN_A_KEYBOARD;
-const int ProximityInfo::MAX_CHAR_CODE;
+int ProximityInfo::getKeyCenterYOfKeyIdG(int keyId) const {
+    if (keyId >= 0) {
+        return mCenterYsG[keyId];
+    }
+    return 0;
+}
 
+int ProximityInfo::getKeyKeyDistanceG(int key0, int key1) const {
+    const int keyId0 = getKeyIndexOf(key0);
+    const int keyId1 = getKeyIndexOf(key1);
+    if (keyId0 >= 0 && keyId1 >= 0) {
+        return mKeyKeyDistancesG[keyId0][keyId1];
+    }
+    return MAX_POINT_TO_KEY_LENGTH;
+}
 } // namespace latinime
diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h
index 3a2511c..0d8c6a3 100644
--- a/native/jni/src/proximity_info.h
+++ b/native/jni/src/proximity_info.h
@@ -18,113 +18,152 @@
 #define LATINIME_PROXIMITY_INFO_H
 
 #include <stdint.h>
-#include <string>
 
 #include "defines.h"
+#include "hash_map_compat.h"
+#include "jni.h"
 
 namespace latinime {
 
 class Correction;
 
+inline bool isSkippableChar(const uint16_t character) {
+    // TODO: Do not hardcode here
+    return character == '\'' || character == '-';
+}
+
 class ProximityInfo {
  public:
-    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2 = 10;
-    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR =
-            1 << NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2;
-
-    // Used as a return value for character comparison
-    typedef enum {
-        // Same char, possibly with different case or accent
-        EQUIVALENT_CHAR,
-        // It is a char located nearby on the keyboard
-        NEAR_PROXIMITY_CHAR,
-        // It is an unrelated char
-        UNRELATED_CHAR,
-        // Additional proximity char which can differ by language.
-        ADDITIONAL_PROXIMITY_CHAR
-    } ProximityType;
-
-    ProximityInfo(const std::string localeStr, const int maxProximityCharsSize,
+    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;
-    void setInputParams(const int32_t *inputCodes, const int inputLength,
-            const int *xCoordinates, const int *yCoordinates);
-    const int* getProximityCharsAt(const int index) const;
-    unsigned short getPrimaryCharAt(const int index) const;
-    bool existsCharInProximityAt(const int index, const int c) const;
-    bool existsAdjacentProximityChars(const int index) const;
-    ProximityType getMatchedProximityId(const int index, const unsigned short c,
-            const bool checkProximityChars, int *proximityIndex = 0) const;
-    int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const {
-        return mNormalizedSquaredDistances[inputIndex * MAX_PROXIMITY_CHARS_SIZE + proximityIndex];
-    }
+    int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const;
+    float getNormalizedSquaredDistanceFromCenterFloat(
+            const int keyId, const int x, const int y) const;
     bool sameAsTyped(const unsigned short *word, int length) const;
-    const unsigned short* getPrimaryInputWord() const {
-        return mPrimaryInputWord;
+    int getKeyIndexOf(const int c) const;
+    int getCodePointOf(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.0f;
     }
-    bool touchPositionCorrectionEnabled() const {
-        return mTouchPositionCorrectionEnabled;
+    float getSweetSpotRadiiAt(int keyIndex) const {
+        return mSweetSpotRadii[keyIndex];
     }
+    float getSweetSpotCenterXAt(int keyIndex) const {
+        return mSweetSpotCenterXs[keyIndex];
+    }
+    float getSweetSpotCenterYAt(int keyIndex) const {
+        return mSweetSpotCenterYs[keyIndex];
+    }
+    void calculateNearbyKeyCodes(
+            const int x, const int y, const int32_t primaryKey, int *inputCodes) const;
+
+    bool hasTouchPositionCorrectionData() const {
+        return HAS_TOUCH_POSITION_CORRECTION_DATA;
+    }
+
+    int getMostCommonKeyWidth() const {
+        return MOST_COMMON_KEY_WIDTH;
+    }
+
+    int getMostCommonKeyWidthSquare() const {
+        return MOST_COMMON_KEY_WIDTH_SQUARE;
+    }
+
+    const char *getLocaleStr() const {
+        return mLocaleStr;
+    }
+
+    int getKeyCount() const {
+        return KEY_COUNT;
+    }
+
+    int getCellHeight() const {
+        return CELL_HEIGHT;
+    }
+
+    int getCellWidth() const {
+        return CELL_WIDTH;
+    }
+
+    int getGridWidth() const {
+        return GRID_WIDTH;
+    }
+
+    int getGridHeight() const {
+        return GRID_HEIGHT;
+    }
+
+    int getKeyboardWidth() const {
+        return KEYBOARD_WIDTH;
+    }
+
+    int getKeyboardHeight() const {
+        return KEYBOARD_HEIGHT;
+    }
+
+    int getKeyCenterXOfCodePointG(int charCode) const;
+    int getKeyCenterYOfCodePointG(int charCode) const;
+    int getKeyCenterXOfKeyIdG(int keyId) const;
+    int getKeyCenterYOfKeyIdG(int keyId) const;
+    int getKeyKeyDistanceG(int key0, int key1) const;
 
  private:
-    // The max number of the keys in one keyboard layout
-    static const int MAX_KEY_COUNT_IN_A_KEYBOARD = 64;
-    // The upper limit of the char code in mCodeToKeyIndex
-    static const int MAX_CHAR_CODE = 127;
+    DISALLOW_IMPLICIT_CONSTRUCTORS(ProximityInfo);
     static const float NOT_A_DISTANCE_FLOAT;
-    static const int NOT_A_CODE = -1;
 
     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 getKeyIndex(const int c) const;
-    bool hasSweetSpotData(const int keyIndex) const {
-        // When there are no calibration data for a key,
-        // the radius of the key is assigned to zero.
-        return mSweetSpotRadii[keyIndex] > 0.0;
-    }
-    bool isOnKey(const int keyId, const int x, const int y) const;
     int squaredDistanceToEdge(const int keyId, const int x, const int y) const;
-    void calculateNearbyKeyCodes(
-            const int x, const int y, const int32_t primaryKey, int *inputCodes) const;
+    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 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 int KEYBOARD_WIDTH;
+    const int KEYBOARD_HEIGHT;
     const bool HAS_TOUCH_POSITION_CORRECTION_DATA;
-    const std::string mLocaleStr;
-    int32_t *mInputCodes;
-    const int *mInputXCoordinates;
-    const int *mInputYCoordinates;
-    bool mTouchPositionCorrectionEnabled;
+    char mLocaleStr[MAX_LOCALE_STRING_LENGTH];
     int32_t *mProximityCharsArray;
-    int *mNormalizedSquaredDistances;
     int32_t mKeyXCoordinates[MAX_KEY_COUNT_IN_A_KEYBOARD];
     int32_t mKeyYCoordinates[MAX_KEY_COUNT_IN_A_KEYBOARD];
     int32_t mKeyWidths[MAX_KEY_COUNT_IN_A_KEYBOARD];
     int32_t mKeyHeights[MAX_KEY_COUNT_IN_A_KEYBOARD];
-    int32_t mKeyCharCodes[MAX_KEY_COUNT_IN_A_KEYBOARD];
+    int32_t mKeyCodePoints[MAX_KEY_COUNT_IN_A_KEYBOARD];
     float mSweetSpotCenterXs[MAX_KEY_COUNT_IN_A_KEYBOARD];
     float mSweetSpotCenterYs[MAX_KEY_COUNT_IN_A_KEYBOARD];
     float mSweetSpotRadii[MAX_KEY_COUNT_IN_A_KEYBOARD];
-    int mInputLength;
-    unsigned short mPrimaryInputWord[MAX_WORD_LENGTH_INTERNAL];
-    int mCodeToKeyIndex[MAX_CHAR_CODE + 1];
+    hash_map_compat<int, int> mCodeToKeyMap;
+
+    int mKeyIndexToCodePointG[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
-
 #endif // LATINIME_PROXIMITY_INFO_H
diff --git a/native/jni/src/proximity_info_state.cpp b/native/jni/src/proximity_info_state.cpp
new file mode 100644
index 0000000..c5f2884
--- /dev/null
+++ b/native/jni/src/proximity_info_state.cpp
@@ -0,0 +1,542 @@
+/*
+ * 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 <cstring> // for memset()
+#include <stdint.h>
+
+#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 {
+
+const int ProximityInfoState::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2 = 10;
+const int ProximityInfoState::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR =
+        1 << NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2;
+const float ProximityInfoState::NOT_A_DISTANCE_FLOAT = -1.0f;
+const int ProximityInfoState::NOT_A_CODE = -1;
+
+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) {
+
+    if (isGeometric) {
+        mIsContinuationPossible = checkAndReturnIsContinuationPossible(
+                inputSize, xCoordinates, yCoordinates, times);
+    } else {
+        mIsContinuationPossible = false;
+    }
+
+    mProximityInfo = proximityInfo;
+    mHasTouchPositionCorrectionData = proximityInfo->hasTouchPositionCorrectionData();
+    mMostCommonKeyWidthSquare = proximityInfo->getMostCommonKeyWidthSquare();
+    mLocaleStr = proximityInfo->getLocaleStr();
+    mKeyCount = proximityInfo->getKeyCount();
+    mCellHeight = proximityInfo->getCellHeight();
+    mCellWidth = proximityInfo->getCellWidth();
+    mGridHeight = proximityInfo->getGridWidth();
+    mGridWidth = proximityInfo->getGridHeight();
+
+    memset(mInputCodes, 0, sizeof(mInputCodes));
+
+    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);
+        }
+
+        if (DEBUG_PROXIMITY_CHARS) {
+            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
+    int pushTouchPointStartIndex = 0;
+    int lastSavedInputSize = 0;
+    mMaxPointToKeyLength = maxPointToKeyLength;
+    if (mIsContinuationPossible && mInputIndice.size() > 1) {
+        // Just update difference.
+        // Two points prior is never skipped. Thus, we pop 2 input point data here.
+        pushTouchPointStartIndex = mInputIndice[mInputIndice.size() - 2];
+        popInputData();
+        popInputData();
+        lastSavedInputSize = mInputXs.size();
+    } else {
+        // Clear all data.
+        mInputXs.clear();
+        mInputYs.clear();
+        mTimes.clear();
+        mInputIndice.clear();
+        mLengthCache.clear();
+        mDistanceCache.clear();
+        mNearKeysVector.clear();
+    }
+    if (DEBUG_GEO_FULL) {
+        AKLOGI("Init ProximityInfoState: reused points =  %d, last input size = %d",
+                pushTouchPointStartIndex, lastSavedInputSize);
+    }
+    mInputSize = 0;
+
+    if (xCoordinates && yCoordinates) {
+        const bool proximityOnly = !isGeometric && (xCoordinates[0] < 0 || yCoordinates[0] < 0);
+        int lastInputIndex = pushTouchPointStartIndex;
+        for (int i = lastInputIndex; i < inputSize; ++i) {
+            const int pid = pointerIds ? pointerIds[i] : 0;
+            if (pointerId == pid) {
+                lastInputIndex = i;
+            }
+        }
+        if (DEBUG_GEO_FULL) {
+            AKLOGI("Init ProximityInfoState: last input index = %d", lastInputIndex);
+        }
+        // Working space to save near keys distances for current, prev and prevprev input point.
+        NearKeysDistanceMap nearKeysDistances[3];
+        // These pointers are swapped for each inputs points.
+        NearKeysDistanceMap *currentNearKeysDistances = &nearKeysDistances[0];
+        NearKeysDistanceMap *prevNearKeysDistances = &nearKeysDistances[1];
+        NearKeysDistanceMap *prevPrevNearKeysDistances = &nearKeysDistances[2];
+
+        for (int i = pushTouchPointStartIndex; i <= lastInputIndex; ++i) {
+            // Assuming pointerId == 0 if pointerIds is null.
+            const int pid = pointerIds ? pointerIds[i] : 0;
+            if (DEBUG_GEO_FULL) {
+                AKLOGI("Init ProximityInfoState: (%d)PID = %d", i, pid);
+            }
+            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(i, c, x, y, time, isGeometric /* do sampling */,
+                        i == lastInputIndex, currentNearKeysDistances, prevNearKeysDistances,
+                        prevPrevNearKeysDistances)) {
+                    // Previous point information was popped.
+                    NearKeysDistanceMap *tmp = prevNearKeysDistances;
+                    prevNearKeysDistances = currentNearKeysDistances;
+                    currentNearKeysDistances = tmp;
+                } else {
+                    NearKeysDistanceMap *tmp = prevPrevNearKeysDistances;
+                    prevPrevNearKeysDistances = prevNearKeysDistances;
+                    prevNearKeysDistances = currentNearKeysDistances;
+                    currentNearKeysDistances = tmp;
+                }
+            }
+        }
+        mInputSize = mInputXs.size();
+    }
+
+    if (mInputSize > 0) {
+        const int keyCount = mProximityInfo->getKeyCount();
+        mNearKeysVector.resize(mInputSize);
+        mDistanceCache.resize(mInputSize * keyCount);
+        for (int i = lastSavedInputSize; i < mInputSize; ++i) {
+            mNearKeysVector[i].reset();
+            static const float NEAR_KEY_NORMALIZED_SQUARED_THRESHOLD = 4.0f;
+            for (int k = 0; k < keyCount; ++k) {
+                const int index = i * keyCount + k;
+                const int x = mInputXs[i];
+                const int y = mInputYs[i];
+                const float normalizedSquaredDistance =
+                        mProximityInfo->getNormalizedSquaredDistanceFromCenterFloat(k, x, y);
+                mDistanceCache[index] = normalizedSquaredDistance;
+                if (normalizedSquaredDistance < NEAR_KEY_NORMALIZED_SQUARED_THRESHOLD) {
+                    mNearKeysVector[i].set(k, 1);
+                }
+            }
+        }
+
+        static const float READ_FORWORD_LENGTH_SCALE = 0.95f;
+        const int readForwordLength = static_cast<int>(
+                hypotf(mProximityInfo->getKeyboardWidth(), mProximityInfo->getKeyboardHeight())
+                * READ_FORWORD_LENGTH_SCALE);
+        for (int i = 0; i < mInputSize; ++i) {
+            if (DEBUG_GEO_FULL) {
+                AKLOGI("Sampled(%d): x = %d, y = %d, time = %d", i, mInputXs[i], mInputYs[i],
+                        mTimes[i]);
+            }
+            for (int j = max(i + 1, lastSavedInputSize); j < mInputSize; ++j) {
+                if (mLengthCache[j] - mLengthCache[i] >= readForwordLength) {
+                    break;
+                }
+                mNearKeysVector[i] |= mNearKeysVector[j];
+            }
+        }
+    }
+
+    // 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->getKeyIndexOf(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);
+                }
+            }
+        }
+    }
+
+    if (DEBUG_GEO_FULL) {
+        AKLOGI("ProximityState init finished: %d points out of %d", mInputSize, inputSize);
+    }
+}
+
+bool ProximityInfoState::checkAndReturnIsContinuationPossible(const int inputSize,
+        const int *const xCoordinates, const int *const yCoordinates, const int *const times) {
+    for (int i = 0; i < mInputSize; ++i) {
+        const int index = mInputIndice[i];
+        if (index > inputSize || xCoordinates[index] != mInputXs[i] ||
+                yCoordinates[index] != mInputYs[i] || times[index] != mTimes[i]) {
+            return false;
+        }
+    }
+    return true;
+}
+
+// Calculating point to key distance for all near keys and returning the distance between
+// the given point and the nearest key position.
+float ProximityInfoState::updateNearKeysDistances(const int x, const int y,
+        NearKeysDistanceMap *const currentNearKeysDistances) {
+    static const float NEAR_KEY_THRESHOLD = 4.0f;
+
+    currentNearKeysDistances->clear();
+    const int keyCount = mProximityInfo->getKeyCount();
+    float nearestKeyDistance = mMaxPointToKeyLength;
+    for (int k = 0; k < keyCount; ++k) {
+        const float dist = mProximityInfo->getNormalizedSquaredDistanceFromCenterFloat(k, x, y);
+        if (dist < NEAR_KEY_THRESHOLD) {
+            currentNearKeysDistances->insert(std::pair<int, float>(k, dist));
+        }
+        if (nearestKeyDistance > dist) {
+            nearestKeyDistance = dist;
+        }
+    }
+    return nearestKeyDistance;
+}
+
+// Check if previous point is at local minimum position to near keys.
+bool ProximityInfoState::isPrevLocalMin(const NearKeysDistanceMap *const currentNearKeysDistances,
+        const NearKeysDistanceMap *const prevNearKeysDistances,
+        const NearKeysDistanceMap *const prevPrevNearKeysDistances) const {
+    static const float MARGIN = 0.01f;
+
+    for (NearKeysDistanceMap::const_iterator it = prevNearKeysDistances->begin();
+        it != prevNearKeysDistances->end(); ++it) {
+        NearKeysDistanceMap::const_iterator itPP = prevPrevNearKeysDistances->find(it->first);
+        NearKeysDistanceMap::const_iterator itC = currentNearKeysDistances->find(it->first);
+        if ((itPP == prevPrevNearKeysDistances->end() || itPP->second > it->second + MARGIN)
+                && (itC == currentNearKeysDistances->end() || itC->second > it->second + MARGIN)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+// Calculating a point score that indicates usefulness of the point.
+float ProximityInfoState::getPointScore(
+        const int x, const int y, const int time, const bool lastPoint, const float nearest,
+        const NearKeysDistanceMap *const currentNearKeysDistances,
+        const NearKeysDistanceMap *const prevNearKeysDistances,
+        const NearKeysDistanceMap *const prevPrevNearKeysDistances) const {
+    static const int DISTANCE_BASE_SCALE = 100;
+    static const int SAVE_DISTANCE_SCALE = 200;
+    static const int SKIP_DISTANCE_SCALE = 25;
+    static const int CHECK_LOCALMIN_DISTANCE_THRESHOLD_SCALE = 40;
+    static const int STRAIGHT_SKIP_DISTANCE_THRESHOLD_SCALE = 50;
+    static const int CORNER_CHECK_DISTANCE_THRESHOLD_SCALE = 27;
+    static const float SAVE_DISTANCE_SCORE = 2.0f;
+    static const float SKIP_DISTANCE_SCORE = -1.0f;
+    static const float CHECK_LOCALMIN_DISTANCE_SCORE = -1.0f;
+    static const float STRAIGHT_ANGLE_THRESHOLD = M_PI_F / 36.0f;
+    static const float STRAIGHT_SKIP_NEAREST_DISTANCE_THRESHOLD = 0.5f;
+    static const float STRAIGHT_SKIP_SCORE = -1.0f;
+    static const float CORNER_ANGLE_THRESHOLD = M_PI_F / 2.0f;
+    static const float CORNER_SCORE = 1.0f;
+
+    const std::size_t size = mInputXs.size();
+    if (size <= 1) {
+        return 0.0f;
+    }
+    const int baseSampleRate = mProximityInfo->getMostCommonKeyWidth();
+    const int distNext = getDistanceInt(x, y, mInputXs.back(), mInputYs.back())
+            * DISTANCE_BASE_SCALE;
+    const int distPrev = getDistanceInt(mInputXs.back(), mInputYs.back(),
+            mInputXs[size - 2], mInputYs[size - 2]) * DISTANCE_BASE_SCALE;
+    float score = 0.0f;
+
+    // Sum of distances
+    if (distPrev + distNext > baseSampleRate * SAVE_DISTANCE_SCALE) {
+        score +=  SAVE_DISTANCE_SCORE;
+    }
+    // Distance
+    if (distPrev < baseSampleRate * SKIP_DISTANCE_SCALE) {
+        score += SKIP_DISTANCE_SCORE;
+    }
+    // Location
+    if (distPrev < baseSampleRate * CHECK_LOCALMIN_DISTANCE_THRESHOLD_SCALE) {
+        if (!isPrevLocalMin(currentNearKeysDistances, prevNearKeysDistances,
+            prevPrevNearKeysDistances)) {
+            score += CHECK_LOCALMIN_DISTANCE_SCORE;
+        }
+    }
+    // Angle
+    const float angle1 = getAngle(x, y, mInputXs.back(), mInputYs.back());
+    const float angle2 = getAngle(mInputXs.back(), mInputYs.back(),
+            mInputXs[size - 2], mInputYs[size - 2]);
+    const float angleDiff = getAngleDiff(angle1, angle2);
+    // Skip straight
+    if (nearest > STRAIGHT_SKIP_NEAREST_DISTANCE_THRESHOLD
+            && distPrev < baseSampleRate * STRAIGHT_SKIP_DISTANCE_THRESHOLD_SCALE
+            && angleDiff < STRAIGHT_ANGLE_THRESHOLD) {
+        score += STRAIGHT_SKIP_SCORE;
+    }
+    // Save corner
+    if (distPrev > baseSampleRate * CORNER_CHECK_DISTANCE_THRESHOLD_SCALE
+            && angleDiff > CORNER_ANGLE_THRESHOLD) {
+        score += CORNER_SCORE;
+    }
+    return score;
+}
+
+// Sampling touch point and pushing information to vectors.
+// Returning if previous point is popped or not.
+bool ProximityInfoState::pushTouchPoint(const int inputIndex, const int nodeChar, int x, int y,
+        const int time, const bool sample, const bool isLastPoint,
+        NearKeysDistanceMap *const currentNearKeysDistances,
+        const NearKeysDistanceMap *const prevNearKeysDistances,
+        const NearKeysDistanceMap *const prevPrevNearKeysDistances) {
+    static const float LAST_POINT_SKIP_DISTANCE_SCALE = 0.25f;
+
+    size_t size = mInputXs.size();
+    bool popped = false;
+    if (nodeChar < 0 && sample) {
+        const float nearest = updateNearKeysDistances(x, y, currentNearKeysDistances);
+        const float score = getPointScore(x, y, time, isLastPoint, nearest,
+                currentNearKeysDistances, prevNearKeysDistances, prevPrevNearKeysDistances);
+        if (score < 0) {
+            // Pop previous point because it would be useless.
+            popInputData();
+            size = mInputXs.size();
+            popped = true;
+        } else {
+            popped = false;
+        }
+        // Check if the last point should be skipped.
+        if (isLastPoint) {
+            if (size > 0 && getDistanceFloat(x, y, mInputXs.back(), mInputYs.back())
+                    < mProximityInfo->getMostCommonKeyWidth() * LAST_POINT_SKIP_DISTANCE_SCALE) {
+                if (DEBUG_GEO_FULL) {
+                    AKLOGI("p0: size = %zd, x = %d, y = %d, lx = %d, ly = %d, dist = %f, "
+                           "width = %f", size, x, y, mInputXs.back(), mInputYs.back(),
+                           getDistanceFloat(x, y, mInputXs.back(), mInputYs.back()),
+                           mProximityInfo->getMostCommonKeyWidth()
+                                   * LAST_POINT_SKIP_DISTANCE_SCALE);
+                }
+                return popped;
+            } else if (size > 1) {
+                int minChar = 0;
+                float minDist = mMaxPointToKeyLength;
+                for (NearKeysDistanceMap::const_iterator it = currentNearKeysDistances->begin();
+                        it != currentNearKeysDistances->end(); ++it) {
+                    if (minDist > it->second) {
+                        minChar = it->first;
+                        minDist = it->second;
+                    }
+                }
+                NearKeysDistanceMap::const_iterator itPP =
+                        prevNearKeysDistances->find(minChar);
+                if (itPP != prevNearKeysDistances->end() && minDist > itPP->second) {
+                    if (DEBUG_GEO_FULL) {
+                        AKLOGI("p1: char = %c, minDist = %f, prevNear key minDist = %f",
+                                minChar, itPP->second, minDist);
+                    }
+                    return popped;
+                }
+            }
+        }
+    }
+
+    if (nodeChar >= 0 && (x < 0 || y < 0)) {
+        const int keyId = mProximityInfo->getKeyIndexOf(nodeChar);
+        if (keyId >= 0) {
+            x = mProximityInfo->getKeyCenterXOfKeyIdG(keyId);
+            y = mProximityInfo->getKeyCenterYOfKeyIdG(keyId);
+        }
+    }
+
+    // Pushing point information.
+    if (size > 0) {
+        mLengthCache.push_back(
+                mLengthCache.back() + getDistanceInt(x, y, mInputXs.back(), mInputYs.back()));
+    } else {
+        mLengthCache.push_back(0);
+    }
+    mInputXs.push_back(x);
+    mInputYs.push_back(y);
+    mTimes.push_back(time);
+    mInputIndice.push_back(inputIndex);
+    if (DEBUG_GEO_FULL) {
+        AKLOGI("pushTouchPoint: x = %03d, y = %03d, time = %d, index = %d, popped ? %01d",
+                x, y, time, inputIndex, popped);
+    }
+    return popped;
+}
+
+float ProximityInfoState::calculateNormalizedSquaredDistance(
+        const int keyIndex, const int inputIndex) const {
+    if (keyIndex == NOT_AN_INDEX) {
+        return NOT_A_DISTANCE_FLOAT;
+    }
+    if (!mProximityInfo->hasSweetSpotData(keyIndex)) {
+        return NOT_A_DISTANCE_FLOAT;
+    }
+    if (NOT_A_COORDINATE == mInputXs[inputIndex]) {
+        return NOT_A_DISTANCE_FLOAT;
+    }
+    const float squaredDistance = calculateSquaredDistanceFromSweetSpotCenter(
+            keyIndex, inputIndex);
+    const float squaredRadius = square(mProximityInfo->getSweetSpotRadiiAt(keyIndex));
+    return squaredDistance / squaredRadius;
+}
+
+int ProximityInfoState::getDuration(const int index) const {
+    if (mInputSize > 0 && index >= 0 && index < mInputSize - 1) {
+        return mTimes[index + 1] - mTimes[index];
+    }
+    return 0;
+}
+
+float ProximityInfoState::getPointToKeyLength(const int inputIndex, const int codePoint,
+        const float scale) const {
+    const int keyId = mProximityInfo->getKeyIndexOf(codePoint);
+    if (keyId != NOT_AN_INDEX) {
+        const int index = inputIndex * mProximityInfo->getKeyCount() + keyId;
+        return min(mDistanceCache[index] * scale, mMaxPointToKeyLength);
+    }
+    if (isSkippableChar(codePoint)) {
+        return 0;
+    }
+    // If the char is not a key on the keyboard then return the max length.
+    return MAX_POINT_TO_KEY_LENGTH;
+}
+
+int ProximityInfoState::getSpaceY() const {
+    const int keyId = mProximityInfo->getKeyIndexOf(' ');
+    return mProximityInfo->getKeyCenterYOfKeyIdG(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>(mInputXs[inputIndex]);
+    const float inputY = static_cast<float>(mInputYs[inputIndex]);
+    return square(inputX - sweetSpotCenterX) + square(inputY - sweetSpotCenterY);
+}
+
+// Puts possible characters into filter and returns new filter size.
+int32_t ProximityInfoState::getAllPossibleChars(
+        const size_t index, int32_t *const filter, const int32_t filterSize) const {
+    if (index >= mInputXs.size()) {
+        return filterSize;
+    }
+    int newFilterSize = filterSize;
+    for (int j = 0; j < mProximityInfo->getKeyCount(); ++j) {
+        if (mNearKeysVector[index].test(j)) {
+            const int32_t keyCodePoint = mProximityInfo->getCodePointOf(j);
+            bool insert = true;
+            // TODO: Avoid linear search
+            for (int k = 0; k < filterSize; ++k) {
+                if (filter[k] == keyCodePoint) {
+                    insert = false;
+                    break;
+                }
+            }
+            if (insert) {
+                filter[newFilterSize++] = keyCodePoint;
+            }
+        }
+    }
+    return newFilterSize;
+}
+
+float ProximityInfoState::getAveragePointDuration() const {
+    if (mInputSize == 0) {
+        return 0.0f;
+    }
+    return static_cast<float>(mTimes[mInputSize - 1] - mTimes[0]) / static_cast<float>(mInputSize);
+}
+
+void ProximityInfoState::popInputData() {
+    mInputXs.pop_back();
+    mInputYs.pop_back();
+    mTimes.pop_back();
+    mLengthCache.pop_back();
+    mInputIndice.pop_back();
+}
+
+} // namespace latinime
diff --git a/native/jni/src/proximity_info_state.h b/native/jni/src/proximity_info_state.h
new file mode 100644
index 0000000..48862a7
--- /dev/null
+++ b/native/jni/src/proximity_info_state.h
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LATINIME_PROXIMITY_INFO_STATE_H
+#define LATINIME_PROXIMITY_INFO_STATE_H
+
+#include <bitset>
+#include <cstring> // for memset()
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#include "char_utils.h"
+#include "defines.h"
+#include "hash_map_compat.h"
+
+namespace latinime {
+
+class ProximityInfo;
+
+class ProximityInfoState {
+ public:
+    typedef std::bitset<MAX_KEY_COUNT_IN_A_KEYBOARD> NearKeycodesSet;
+    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR_LOG_2;
+    static const int NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
+    static const float NOT_A_DISTANCE_FLOAT;
+    static const int NOT_A_CODE;
+
+    /////////////////////////////////////////
+    // Defined in proximity_info_state.cpp //
+    /////////////////////////////////////////
+    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()
+            : mProximityInfo(0), mMaxPointToKeyLength(0),
+              mHasTouchPositionCorrectionData(false), mMostCommonKeyWidthSquare(0), mLocaleStr(),
+              mKeyCount(0), mCellHeight(0), mCellWidth(0), mGridHeight(0), mGridWidth(0),
+              mIsContinuationPossible(false), mInputXs(), mInputYs(), mTimes(), mInputIndice(),
+              mDistanceCache(), mLengthCache(), mNearKeysVector(),
+              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];
+    }
+
+    inline bool existsCharInProximityAt(const int index, const int c) const {
+        const int *chars = getProximityCharsAt(index);
+        int i = 0;
+        while (chars[i] > 0 && i < MAX_PROXIMITY_CHARS_SIZE_INTERNAL) {
+            if (chars[i++] == c) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    inline bool existsAdjacentProximityChars(const int index) const {
+        if (index < 0 || index >= 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 < mInputSize && existsCharInProximityAt(rightIndex, currentChar)) {
+            return true;
+        }
+        return false;
+    }
+
+    // In the following function, c is the current character of the dictionary word
+    // currently examined.
+    // currentChars is an array containing the keys close to the character the
+    // user actually typed at the same position. We want to see if c is in it: if so,
+    // then the word contains at that position a character close to what the user
+    // typed.
+    // What the user typed is actually the first character of the array.
+    // proximityIndex is a pointer to the variable where getMatchedProximityId returns
+    // the index of c in the proximity chars of the input index.
+    // Notice : accented characters do not have a proximity list, so they are alone
+    // in their list. The non-accented version of the character should be considered
+    // "close", but not the other keys close to the non-accented version.
+    inline ProximityType getMatchedProximityId(const int index,
+            const unsigned short c, const bool checkProximityChars, int *proximityIndex = 0) const {
+        const int *currentChars = getProximityCharsAt(index);
+        const int firstChar = currentChars[0];
+        const unsigned short baseLowerC = toBaseLowerCase(c);
+
+        // The first char in the array is what user typed. If it matches right away,
+        // that means the user typed that same char for this pos.
+        if (firstChar == baseLowerC || firstChar == c) {
+            return EQUIVALENT_CHAR;
+        }
+
+        if (!checkProximityChars) return UNRELATED_CHAR;
+
+        // If the non-accented, lowercased version of that first character matches c,
+        // then we have a non-accented version of the accented character the user
+        // typed. Treat it as a close char.
+        if (toBaseLowerCase(firstChar) == baseLowerC)
+            return NEAR_PROXIMITY_CHAR;
+
+        // Not an exact nor an accent-alike match: search the list of close keys
+        int j = 1;
+        while (j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+                && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+            const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
+            if (matched) {
+                if (proximityIndex) {
+                    *proximityIndex = j;
+                }
+                return NEAR_PROXIMITY_CHAR;
+            }
+            ++j;
+        }
+        if (j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+                && currentChars[j] == ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+            ++j;
+            while (j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+                    && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+                const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
+                if (matched) {
+                    if (proximityIndex) {
+                        *proximityIndex = j;
+                    }
+                    return ADDITIONAL_PROXIMITY_CHAR;
+                }
+                ++j;
+            }
+        }
+
+        // Was not included, signal this as an unrelated character.
+        return UNRELATED_CHAR;
+    }
+
+    inline int getNormalizedSquaredDistance(
+            const int inputIndex, const int proximityIndex) const {
+        return mNormalizedSquaredDistances[
+                inputIndex * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + proximityIndex];
+    }
+
+    inline const unsigned short *getPrimaryInputWord() const {
+        return mPrimaryInputWord;
+    }
+
+    inline bool touchPositionCorrectionEnabled() const {
+        return mTouchPositionCorrectionEnabled;
+    }
+
+    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(const int index) const {
+        return mInputXs[index];
+    }
+
+    int getInputY(const int index) const {
+        return mInputYs[index];
+    }
+
+    int getLengthCache(const int index) const {
+        return mLengthCache[index];
+    }
+
+    bool isContinuationPossible() const {
+        return mIsContinuationPossible;
+    }
+
+    float getPointToKeyLength(const int inputIndex, const int charCode, const float scale) const;
+
+    int getSpaceY() const;
+
+    int32_t getAllPossibleChars(
+            const size_t startIndex, int32_t *const filter, const int32_t filterSize) const;
+
+    float getAveragePointDuration() const;
+ private:
+    DISALLOW_COPY_AND_ASSIGN(ProximityInfoState);
+    typedef hash_map_compat<int, float> NearKeysDistanceMap;
+    /////////////////////////////////////////
+    // Defined in proximity_info_state.cpp //
+    /////////////////////////////////////////
+    float calculateNormalizedSquaredDistance(const int keyIndex, const int inputIndex) const;
+
+    float calculateSquaredDistanceFromSweetSpotCenter(
+            const int keyIndex, const int inputIndex) const;
+
+    bool pushTouchPoint(const int inputIndex, const int nodeChar, int x, int y, const int time,
+            const bool sample, const bool isLastPoint,
+            NearKeysDistanceMap *const currentNearKeysDistances,
+            const NearKeysDistanceMap *const prevNearKeysDistances,
+            const NearKeysDistanceMap *const prevPrevNearKeysDistances);
+    /////////////////////////////////////////
+    // Defined here                        //
+    /////////////////////////////////////////
+    inline float square(const float x) const { return x * x; }
+
+    bool hasInputCoordinates() const {
+        return mInputXs.size() > 0 && mInputYs.size() > 0;
+    }
+
+    inline const int *getProximityCharsAt(const int index) const {
+        return mInputCodes + (index * MAX_PROXIMITY_CHARS_SIZE_INTERNAL);
+    }
+
+    float updateNearKeysDistances(const int x, const int y,
+            NearKeysDistanceMap *const currentNearKeysDistances);
+    bool isPrevLocalMin(const NearKeysDistanceMap *const currentNearKeysDistances,
+            const NearKeysDistanceMap *const prevNearKeysDistances,
+            const NearKeysDistanceMap *const prevPrevNearKeysDistances) const;
+    float getPointScore(
+            const int x, const int y, const int time, const bool last, const float nearest,
+            const NearKeysDistanceMap *const currentNearKeysDistances,
+            const NearKeysDistanceMap *const prevNearKeysDistances,
+            const NearKeysDistanceMap *const prevPrevNearKeysDistances) const;
+    bool checkAndReturnIsContinuationPossible(const int inputSize, const int *const xCoordinates,
+            const int *const yCoordinates, const int *const times);
+    void popInputData();
+
+    // const
+    const ProximityInfo *mProximityInfo;
+    float mMaxPointToKeyLength;
+    bool mHasTouchPositionCorrectionData;
+    int mMostCommonKeyWidthSquare;
+    std::string mLocaleStr;
+    int mKeyCount;
+    int mCellHeight;
+    int mCellWidth;
+    int mGridHeight;
+    int mGridWidth;
+    bool mIsContinuationPossible;
+
+    std::vector<int> mInputXs;
+    std::vector<int> mInputYs;
+    std::vector<int> mTimes;
+    std::vector<int> mInputIndice;
+    std::vector<float> mDistanceCache;
+    std::vector<int>  mLengthCache;
+    std::vector<NearKeycodesSet> mNearKeysVector;
+    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 mInputSize;
+    unsigned short mPrimaryInputWord[MAX_WORD_LENGTH_INTERNAL];
+};
+} // namespace latinime
+#endif // LATINIME_PROXIMITY_INFO_STATE_H
diff --git a/native/jni/src/terminal_attributes.h b/native/jni/src/terminal_attributes.h
index 9a803cc..53ae385 100644
--- a/native/jni/src/terminal_attributes.h
+++ b/native/jni/src/terminal_attributes.h
@@ -17,7 +17,7 @@
 #ifndef LATINIME_TERMINAL_ATTRIBUTES_H
 #define LATINIME_TERMINAL_ATTRIBUTES_H
 
-#include "unigram_dictionary.h"
+#include "binary_format.h"
 
 namespace latinime {
 
@@ -29,14 +29,14 @@
 class TerminalAttributes {
  public:
     class ShortcutIterator {
-        const uint8_t* const mDict;
-        bool mHasNextShortcutTarget;
+        const uint8_t *const mDict;
         int mPos;
+        bool mHasNextShortcutTarget;
 
      public:
-        ShortcutIterator(const uint8_t* dict, const int pos, const uint8_t flags) : mDict(dict),
-                mPos(pos) {
-            mHasNextShortcutTarget = (0 != (flags & UnigramDictionary::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,29 +46,24 @@
         // 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 & UnigramDictionary::FLAG_ATTRIBUTE_HAS_NEXT);
+                    0 != (shortcutFlags & BinaryFormat::FLAG_ATTRIBUTE_HAS_NEXT);
             unsigned int i;
             for (i = 0; i < MAX_WORD_LENGTH_INTERNAL; ++i) {
-                const int charCode = BinaryFormat::getCharCodeAndForwardPointer(mDict, &mPos);
-                if (NOT_A_CHARACTER == charCode) break;
-                outWord[i] = (uint16_t)charCode;
+                const int codePoint = BinaryFormat::getCodePointAndForwardPointer(mDict, &mPos);
+                if (NOT_A_CODE_POINT == codePoint) break;
+                outWord[i] = (uint16_t)codePoint;
             }
+            *outFreq = BinaryFormat::getAttributeFrequencyFromFlags(shortcutFlags);
             mPos += BinaryFormat::CHARACTER_ARRAY_TERMINATOR_SIZE;
             return i;
         }
     };
 
- private:
-    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) {
+    TerminalAttributes(const uint8_t *const dict, const uint8_t flags, const int pos)
+            : mDict(dict), mFlags(flags), mStartPos(pos) {
     }
 
     inline ShortcutIterator getShortcutIterator() const {
@@ -76,7 +71,16 @@
         // skipped quickly, so we ignore it.
         return ShortcutIterator(mDict, mStartPos + BinaryFormat::SHORTCUT_LIST_SIZE_SIZE, mFlags);
     }
+
+    bool isBlacklistedOrNotAWord() const {
+        return mFlags & (BinaryFormat::FLAG_IS_BLACKLISTED | BinaryFormat::FLAG_IS_NOT_A_WORD);
+    }
+
+ 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 ea9f11b..49d044f 100644
--- a/native/jni/src/unigram_dictionary.cpp
+++ b/native/jni/src/unigram_dictionary.cpp
@@ -1,32 +1,33 @@
 /*
-**
-** Copyright 2010, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
+ * Copyright (C) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-#include <assert.h>
-#include <string.h>
+#include <cassert>
+#include <cstring>
 
 #define LOG_TAG "LatinIME: unigram_dictionary.cpp"
 
+#include "binary_format.h"
 #include "char_utils.h"
 #include "defines.h"
 #include "dictionary.h"
-#include "unigram_dictionary.h"
-
-#include "binary_format.h"
+#include "proximity_info.h"
 #include "terminal_attributes.h"
+#include "unigram_dictionary.h"
+#include "words_priority_queue.h"
+#include "words_priority_queue_pool.h"
 
 namespace latinime {
 
@@ -40,7 +41,7 @@
         { 'o', 'e', 0x0153 } }; // U+0153 : LATIN SMALL LIGATURE OE
 
 // TODO: check the header
-UnigramDictionary::UnigramDictionary(const uint8_t* const streamStart, int typedLetterMultiplier,
+UnigramDictionary::UnigramDictionary(const uint8_t *const streamStart, int typedLetterMultiplier,
         int fullWordMultiplier, int maxWordLength, int maxWords, const unsigned int flags)
     : DICT_ROOT(streamStart), MAX_WORD_LENGTH(maxWordLength), MAX_WORDS(maxWords),
     TYPED_LETTER_MULTIPLIER(typedLetterMultiplier), FULL_WORD_MULTIPLIER(fullWordMultiplier),
@@ -57,18 +58,18 @@
 }
 
 static inline unsigned int getCodesBufferSize(const int *codes, const int codesSize) {
-    return sizeof(*codes) * codesSize;
+    return static_cast<unsigned int>(sizeof(*codes)) * codesSize;
 }
 
 // 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);
+static inline void addWord(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.
 int UnigramDictionary::getDigraphReplacement(const int *codes, const int i, const int codesSize,
-        const digraph_t* const digraphs, const unsigned int digraphsSize) const {
+        const digraph_t *const digraphs, const unsigned int digraphsSize) const {
 
     // There can't be a digraph if we don't have at least 2 characters to examine
     if (i + 2 > codesSize) return false;
@@ -103,9 +104,9 @@
         const bool useFullEditDistance, const int *codesSrc,
         const int codesRemain, const int currentDepth, int *codesDest, Correction *correction,
         WordsPriorityQueuePool *queuePool,
-        const digraph_t* const digraphs, const unsigned int digraphsSize) {
+        const digraph_t *const digraphs, const unsigned int digraphsSize) const {
 
-    const int startIndex = codesDest - codesBuffer;
+    const int startIndex = static_cast<int>(codesDest - codesBuffer);
     if (currentDepth < MAX_DIGRAPH_SEARCH_DEPTH) {
         for (int i = 0; i < codesRemain; ++i) {
             xCoordinatesBuffer[startIndex + i] = xcoordinates[codesBufferSize - codesRemain + i];
@@ -169,15 +170,16 @@
 // bigramMap contains the association <bigram address> -> <bigram frequency>
 // bigramFilter is a bloom filter for fast rejection: see functions setInFilter and isInFilter
 // in bigram_dictionary.cpp
-int UnigramDictionary::getSuggestions(ProximityInfo *proximityInfo,
-        WordsPriorityQueuePool *queuePool, Correction *correction, const int *xcoordinates,
+int UnigramDictionary::getSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
         const int *ycoordinates, const int *codes, const int codesSize,
         const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
-        const bool useFullEditDistance, unsigned short *outWords, int *frequencies) {
+        const bool useFullEditDistance, unsigned short *outWords, int *frequencies,
+        int *outputTypes) const {
 
-    queuePool->clearAll();
-    Correction* masterCorrection = correction;
-    correction->resetCorrection();
+    WordsPriorityQueuePool queuePool(MAX_WORDS, SUB_QUEUE_MAX_WORDS, MAX_WORD_LENGTH);
+    queuePool.clearAll();
+    Correction masterCorrection;
+    masterCorrection.resetCorrection();
     if (BinaryFormat::REQUIRES_GERMAN_UMLAUT_PROCESSING & FLAGS)
     { // Incrementally tune the word and try all possibilities
         int codesBuffer[getCodesBufferSize(codes, codesSize)];
@@ -185,8 +187,8 @@
         int yCoordinatesBuffer[codesSize];
         getWordWithDigraphSuggestionsRec(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
                 xCoordinatesBuffer, yCoordinatesBuffer, codesSize, bigramMap, bigramFilter,
-                useFullEditDistance, codes, codesSize, 0, codesBuffer, masterCorrection,
-                queuePool, GERMAN_UMLAUT_DIGRAPHS,
+                useFullEditDistance, codes, codesSize, 0, codesBuffer, &masterCorrection,
+                &queuePool, GERMAN_UMLAUT_DIGRAPHS,
                 sizeof(GERMAN_UMLAUT_DIGRAPHS) / sizeof(GERMAN_UMLAUT_DIGRAPHS[0]));
     } else if (BinaryFormat::REQUIRES_FRENCH_LIGATURES_PROCESSING & FLAGS) {
         int codesBuffer[getCodesBufferSize(codes, codesSize)];
@@ -194,36 +196,36 @@
         int yCoordinatesBuffer[codesSize];
         getWordWithDigraphSuggestionsRec(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
                 xCoordinatesBuffer, yCoordinatesBuffer, codesSize, bigramMap, bigramFilter,
-                useFullEditDistance, codes, codesSize, 0, codesBuffer, masterCorrection,
-                queuePool, FRENCH_LIGATURES_DIGRAPHS,
+                useFullEditDistance, codes, codesSize, 0, codesBuffer, &masterCorrection,
+                &queuePool, FRENCH_LIGATURES_DIGRAPHS,
                 sizeof(FRENCH_LIGATURES_DIGRAPHS) / sizeof(FRENCH_LIGATURES_DIGRAPHS[0]));
     } else { // Normal processing
         getWordSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, codesSize,
-                bigramMap, bigramFilter, useFullEditDistance, masterCorrection, queuePool);
+                bigramMap, bigramFilter, useFullEditDistance, &masterCorrection, &queuePool);
     }
 
     PROF_START(20);
     if (DEBUG_DICT) {
-        float ns = queuePool->getMasterQueue()->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), codesSize, 0, 0, 0);
+        float ns = queuePool.getMasterQueue()->getHighestNormalizedScore(
+                masterCorrection.getPrimaryInputWord(), codesSize, 0, 0, 0);
         ns += 0;
         AKLOGI("Max normalized score = %f", ns);
     }
     const int suggestedWordsCount =
-            queuePool->getMasterQueue()->outputSuggestions(
-                    proximityInfo->getPrimaryInputWord(), codesSize, frequencies, outWords);
+            queuePool.getMasterQueue()->outputSuggestions(masterCorrection.getPrimaryInputWord(),
+                    codesSize, frequencies, outWords, outputTypes);
 
     if (DEBUG_DICT) {
-        float ns = queuePool->getMasterQueue()->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), codesSize, 0, 0, 0);
+        float ns = queuePool.getMasterQueue()->getHighestNormalizedScore(
+                masterCorrection.getPrimaryInputWord(), codesSize, 0, 0, 0);
         ns += 0;
         AKLOGI("Returning %d words", suggestedWordsCount);
         /// Print the returned words
         for (int j = 0; j < suggestedWordsCount; ++j) {
-            short unsigned int* w = outWords + j * MAX_WORD_LENGTH;
+            short unsigned int *w = outWords + j * MAX_WORD_LENGTH;
             char s[MAX_WORD_LENGTH];
             for (int i = 0; i <= MAX_WORD_LENGTH; i++) s[i] = w[i];
-            (void)s;
+            (void)s; // To suppress compiler warning
             AKLOGI("%s %i", s, frequencies[j]);
         }
     }
@@ -234,8 +236,9 @@
 
 void UnigramDictionary::getWordSuggestions(ProximityInfo *proximityInfo,
         const int *xcoordinates, const int *ycoordinates, const int *codes,
-        const int inputLength, const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
-        const bool useFullEditDistance, Correction *correction, WordsPriorityQueuePool *queuePool) {
+        const int inputSize, const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
+        const bool useFullEditDistance, Correction *correction,
+        WordsPriorityQueuePool *queuePool) const {
 
     PROF_OPEN;
     PROF_START(0);
@@ -243,7 +246,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);
@@ -256,10 +259,10 @@
 
     PROF_START(4);
     bool hasAutoCorrectionCandidate = false;
-    WordsPriorityQueue* masterQueue = queuePool->getMasterQueue();
+    WordsPriorityQueue *masterQueue = queuePool->getMasterQueue();
     if (masterQueue->size() > 0) {
         float nsForMaster = masterQueue->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), inputLength, 0, 0, 0);
+                correction->getPrimaryInputWord(), inputSize, 0, 0, 0);
         hasAutoCorrectionCandidate = (nsForMaster > START_TWO_WORDS_CORRECTION_THRESHOLD);
     }
     PROF_END(4);
@@ -267,9 +270,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);
@@ -281,18 +284,18 @@
     if (DEBUG_DICT) {
         queuePool->dumpSubQueue1TopSuggestions();
         for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
-            WordsPriorityQueue* queue = queuePool->getSubQueue(FIRST_WORD_INDEX, i);
+            WordsPriorityQueue *queue = queuePool->getSubQueue(FIRST_WORD_INDEX, i);
             if (queue->size() > 0) {
-                WordsPriorityQueue::SuggestedWord* sw = queue->top();
+                WordsPriorityQueue::SuggestedWord *sw = queue->top();
                 const int score = sw->mScore;
-                const unsigned short* word = sw->mWord;
+                const unsigned short *word = sw->mWord;
                 const int wordLength = sw->mWordLength;
                 float ns = Correction::RankingAlgorithm::calcNormalizedScore(
-                        proximityInfo->getPrimaryInputWord(), i, word, wordLength, score);
+                        correction->getPrimaryInputWord(), i, word, wordLength, score);
                 ns += 0;
                 AKLOGI("--- TOP SUB WORDS for %d --- %d %f [%d]", i, score, ns,
                         (ns > TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD));
-                DUMP_WORD(proximityInfo->getPrimaryInputWord(), i);
+                DUMP_WORD(correction->getPrimaryInputWord(), i);
                 DUMP_WORD(word, wordLength);
             }
         }
@@ -300,33 +303,33 @@
 }
 
 void UnigramDictionary::initSuggestions(ProximityInfo *proximityInfo, const int *xCoordinates,
-        const int *yCoordinates, const int *codes, const int inputLength, Correction *correction) {
+        const int *yCoordinates, const int *codes, const int inputSize,
+        Correction *correction) const {
     if (DEBUG_DICT) {
         AKLOGI("initSuggest");
-        DUMP_WORD_INT(codes, inputLength);
+        DUMP_WORD_INT(codes, inputSize);
     }
-    proximityInfo->setInputParams(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 = '\'';
 static const char SPACE = ' ';
 
 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,
-        Correction *correction, WordsPriorityQueuePool *queuePool) {
-    initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, inputLength, correction);
-    getSuggestionCandidates(useFullEditDistance, inputLength, bigramMap, bigramFilter, correction,
+        const bool useFullEditDistance, const int inputSize,
+        Correction *correction, WordsPriorityQueuePool *queuePool) const {
+    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 bool doAutoCompletion, const int maxErrors, const int currentWordIndex) const {
     uint8_t totalTraverseCount = correction->pushAndGetTotalTraverseCount();
     if (DEBUG_DICT) {
         AKLOGI("Traverse count %d", totalTraverseCount);
@@ -346,7 +349,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) {
@@ -374,38 +377,55 @@
 inline void UnigramDictionary::onTerminal(const int probability,
         const TerminalAttributes& terminalAttributes, Correction *correction,
         WordsPriorityQueuePool *queuePool, const bool addToMasterQueue,
-        const int currentWordIndex) {
+        const int currentWordIndex) const {
     const int inputIndex = correction->getInputIndex();
     const bool addToSubQueue = inputIndex < SUB_QUEUE_MAX_COUNT;
 
     int wordLength;
-    unsigned short* wordPointer;
+    unsigned short *wordPointer;
 
     if ((currentWordIndex == FIRST_WORD_INDEX) && addToMasterQueue) {
         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 && !terminalAttributes.isBlacklistedOrNotAWord()) {
+            // 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.
+            // Furthermore, if this is not a word (shortcut only for example) or a blacklisted
+            // entry then we never want to suggest this.
+            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);
         }
     }
 
@@ -419,18 +439,18 @@
         }
         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,
-        int*wordLengthArray, unsigned short* outputWord, int *outputWordLength) {
+        int *wordLengthArray, unsigned short *outputWord, int *outputWordLength) const {
     if (inputWordLength > MULTIPLE_WORDS_SUGGESTION_MAX_WORD_LENGTH) {
         return FLAG_MULTIPLE_SUGGEST_ABORT;
     }
@@ -473,17 +493,18 @@
     // TODO: Remove the safety net above        //
     //////////////////////////////////////////////
 
-    unsigned short* tempOutputWord = 0;
+    unsigned short *tempOutputWord = 0;
     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(
-            inputWordStartPos, inputWordLength, proximityInfo, mWord);
+            inputWordStartPos, inputWordLength, correction, word);
     if (freq > 0) {
         nextWordLength = inputWordLength;
-        tempOutputWord = mWord;
+        tempOutputWord = word;
     } else if (!hasAutoCorrectionCandidate) {
         if (inputWordStartPos > 0) {
             const int offset = inputWordStartPos;
@@ -503,14 +524,14 @@
                 }
             }
         }
-        WordsPriorityQueue* queue = queuePool->getSubQueue(currentWordIndex, inputWordLength);
+        WordsPriorityQueue *queue = queuePool->getSubQueue(currentWordIndex, inputWordLength);
         // TODO: Return the correct value depending on doAutoCompletion
         if (!queue || queue->size() <= 0) {
             return FLAG_MULTIPLE_SUGGEST_ABORT;
         }
         int score = 0;
         const float ns = queue->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), inputWordLength,
+                correction->getPrimaryInputWord(), inputWordLength,
                 &tempOutputWord, &score, &nextWordLength);
         if (DEBUG_DICT) {
             AKLOGI("NS(%d) = %f, Score = %d", currentWordIndex, ns, score);
@@ -524,9 +545,9 @@
         freq = score >> (nextWordLength + TWO_WORDS_PLUS_OTHER_ERROR_CORRECTION_DEMOTION_DIVIDER);
     }
     if (DEBUG_DICT) {
-        AKLOGI("Freq(%d): %d, length: %d, input length: %d, input start: %d (%d)"
-                , currentWordIndex, freq, nextWordLength, inputWordLength, inputWordStartPos,
-                wordLengthArray[0]);
+        AKLOGI("Freq(%d): %d, length: %d, input length: %d, input start: %d (%d)",
+                currentWordIndex, freq, nextWordLength, inputWordLength, inputWordStartPos,
+                (currentWordIndex > 0) ? wordLengthArray[0] : 0);
     }
     if (freq <= 0 || nextWordLength <= 0
             || MAX_WORD_LENGTH <= (outputWordStartPos + nextWordLength)) {
@@ -545,7 +566,7 @@
         *outputWordLength = tempOutputWordLength;
     }
 
-    if ((inputWordStartPos + inputWordLength) < inputLength) {
+    if ((inputWordStartPos + inputWordLength) < inputSize) {
         if (outputWordStartPos + nextWordLength >= MAX_WORD_LENGTH) {
             return FLAG_MULTIPLE_SUGGEST_SKIP;
         }
@@ -564,31 +585,31 @@
                         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,
-        Correction *correction, WordsPriorityQueuePool* queuePool,
-        const bool hasAutoCorrectionCandidate, const int startInputPos, const int startWordIndex,
-        const int outputWordLength, int *freqArray, int* wordLengthArray,
-        unsigned short* outputWord) {
+        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, unsigned short *outputWord) const {
     if (startWordIndex >= (MULTIPLE_WORDS_SUGGESTION_MAX_WORDS - 1)) {
         // Return if the last word index
         return;
     }
     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);
@@ -599,7 +620,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);
@@ -616,14 +637,14 @@
         // Next word
         // Missing space
         inputWordStartPos = i;
-        inputWordLength = inputLength - i;
-        if(getSubStringSuggestion(proximityInfo, xcoordinates, ycoordinates, codes,
-                useFullEditDistance, correction, queuePool, inputLength, hasAutoCorrectionCandidate,
+        inputWordLength = inputSize - i;
+        if (getSubStringSuggestion(proximityInfo, xcoordinates, ycoordinates, codes,
+                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);
         }
@@ -646,7 +667,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);
     }
@@ -654,10 +675,10 @@
 
 void UnigramDictionary::getSplitMultipleWordsSuggestions(ProximityInfo *proximityInfo,
         const int *xcoordinates, const int *ycoordinates, const int *codes,
-        const bool useFullEditDistance, const int inputLength,
-        Correction *correction, WordsPriorityQueuePool* queuePool,
-        const bool hasAutoCorrectionCandidate) {
-    if (inputLength >= MAX_WORD_LENGTH) return;
+        const bool useFullEditDistance, const int inputSize,
+        Correction *correction, WordsPriorityQueuePool *queuePool,
+        const bool hasAutoCorrectionCandidate) const {
+    if (inputSize >= MAX_WORD_LENGTH) return;
     if (DEBUG_DICT) {
         AKLOGI("--- Suggest multiple words");
     }
@@ -670,7 +691,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);
 }
@@ -678,13 +699,13 @@
 // Wrapper for getMostFrequentWordLikeInner, which matches it to the previous
 // interface.
 inline int UnigramDictionary::getMostFrequentWordLike(const int startInputIndex,
-        const int inputLength, ProximityInfo *proximityInfo, unsigned short *word) {
-    uint16_t inWord[inputLength];
+        const int inputSize, Correction *correction, unsigned short *word) const {
+    uint16_t inWord[inputSize];
 
-    for (int i = 0; i < inputLength; ++i) {
-        inWord[i] = (uint16_t)proximityInfo->getPrimaryCharAt(startInputIndex + 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,
@@ -700,13 +721,13 @@
 // In and out parameters may point to the same location. This function takes care
 // not to use any input parameters after it wrote into its outputs.
 static inline bool testCharGroupForContinuedLikeness(const uint8_t flags,
-        const uint8_t* const root, const int startPos,
-        const uint16_t* const inWord, const int startInputIndex,
-        int32_t* outNewWord, int* outInputIndex, int* outPos) {
-    const bool hasMultipleChars = (0 != (UnigramDictionary::FLAG_HAS_MULTIPLE_CHARS & flags));
+        const uint8_t *const root, const int startPos, const uint16_t *const inWord,
+        const int startInputIndex, const int inputSize, int32_t *outNewWord, int *outInputIndex,
+        int *outPos) {
+    const bool hasMultipleChars = (0 != (BinaryFormat::FLAG_HAS_MULTIPLE_CHARS & flags));
     int pos = startPos;
-    int32_t character = BinaryFormat::getCharCodeAndForwardPointer(root, &pos);
-    int32_t baseChar = toBaseLowerCase(character);
+    int32_t codePoint = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
+    int32_t baseChar = toBaseLowerCase(codePoint);
     const uint16_t wChar = toBaseLowerCase(inWord[startInputIndex]);
 
     if (baseChar != wChar) {
@@ -715,18 +736,18 @@
         return false;
     }
     int inputIndex = startInputIndex;
-    outNewWord[inputIndex] = character;
+    outNewWord[inputIndex] = codePoint;
     if (hasMultipleChars) {
-        character = BinaryFormat::getCharCodeAndForwardPointer(root, &pos);
-        while (NOT_A_CHARACTER != character) {
-            baseChar = toBaseLowerCase(character);
-            if (toBaseLowerCase(inWord[++inputIndex]) != baseChar) {
+        codePoint = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
+        while (NOT_A_CODE_POINT != codePoint) {
+            baseChar = toBaseLowerCase(codePoint);
+            if (inputIndex + 1 >= inputSize || toBaseLowerCase(inWord[++inputIndex]) != baseChar) {
                 *outPos = BinaryFormat::skipOtherCharacters(root, pos);
                 *outInputIndex = startInputIndex;
                 return false;
             }
-            outNewWord[inputIndex] = character;
-            character = BinaryFormat::getCharCodeAndForwardPointer(root, &pos);
+            outNewWord[inputIndex] = codePoint;
+            codePoint = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
         }
     }
     *outInputIndex = inputIndex + 1;
@@ -738,11 +759,12 @@
 // It will compare the frequency to the max frequency, and if greater, will
 // copy the word into the output buffer. In output value maxFreq, it will
 // write the new maximum frequency if it changed.
-static inline void onTerminalWordLike(const int freq, int32_t* newWord, const int length,
-        short unsigned int* outWord, int* maxFreq) {
+static inline void onTerminalWordLike(const int freq, int32_t *newWord, const int length,
+        short unsigned int *outWord, int *maxFreq) {
     if (freq > *maxFreq) {
-        for (int q = 0; q < length; ++q)
+        for (int q = 0; q < length; ++q) {
             outWord[q] = newWord[q];
+        }
         outWord[length] = 0;
         *maxFreq = freq;
     }
@@ -750,30 +772,33 @@
 
 // Will find the highest frequency of the words like the one passed as an argument,
 // that is, everything that only differs by case/accents.
-int UnigramDictionary::getMostFrequentWordLikeInner(const uint16_t * const inWord,
-        const int length, short unsigned int* outWord) {
+int UnigramDictionary::getMostFrequentWordLikeInner(const uint16_t *const inWord,
+        const int inputSize, short unsigned int *outWord) const {
     int32_t newWord[MAX_WORD_LENGTH_INTERNAL];
     int depth = 0;
     int maxFreq = -1;
-    const uint8_t* const root = DICT_ROOT;
+    const uint8_t *const root = DICT_ROOT;
+    int stackChildCount[MAX_WORD_LENGTH_INTERNAL];
+    int stackInputIndex[MAX_WORD_LENGTH_INTERNAL];
+    int stackSiblingPos[MAX_WORD_LENGTH_INTERNAL];
 
     int startPos = 0;
-    mStackChildCount[0] = BinaryFormat::getGroupCountAndForwardPointer(root, &startPos);
-    mStackInputIndex[0] = 0;
-    mStackSiblingPos[0] = startPos;
+    stackChildCount[0] = BinaryFormat::getGroupCountAndForwardPointer(root, &startPos);
+    stackInputIndex[0] = 0;
+    stackSiblingPos[0] = startPos;
     while (depth >= 0) {
-        const int charGroupCount = mStackChildCount[depth];
-        int pos = mStackSiblingPos[depth];
+        const int charGroupCount = stackChildCount[depth];
+        int pos = stackSiblingPos[depth];
         for (int charGroupIndex = charGroupCount - 1; charGroupIndex >= 0; --charGroupIndex) {
-            int inputIndex = mStackInputIndex[depth];
+            int inputIndex = stackInputIndex[depth];
             const uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
             // Test whether all chars in this group match with the word we are searching for. If so,
-            // we want to traverse its children (or if the length match, evaluate its frequency).
+            // we want to traverse its children (or if the inputSize match, evaluate its frequency).
             // Note that this function will output the position regardless, but will only write
             // into inputIndex if there is a match.
             const bool isAlike = testCharGroupForContinuedLikeness(flags, root, pos, inWord,
-                    inputIndex, newWord, &inputIndex, &pos);
-            if (isAlike && (FLAG_IS_TERMINAL & flags) && (inputIndex == length)) {
+                    inputIndex, inputSize, newWord, &inputIndex, &pos);
+            if (isAlike && (BinaryFormat::FLAG_IS_TERMINAL & flags) && (inputIndex == inputSize)) {
                 const int frequency = BinaryFormat::readFrequencyWithoutMovingPointer(root, pos);
                 onTerminalWordLike(frequency, newWord, inputIndex, outWord, &maxFreq);
             }
@@ -782,18 +807,18 @@
             const int childrenNodePos = BinaryFormat::readChildrenPosition(root, flags, pos);
             // If we had a match and the word has children, we want to traverse them. We don't have
             // to traverse words longer than the one we are searching for, since they will not match
-            // anyway, so don't traverse unless inputIndex < length.
-            if (isAlike && (-1 != childrenNodePos) && (inputIndex < length)) {
+            // anyway, so don't traverse unless inputIndex < inputSize.
+            if (isAlike && (-1 != childrenNodePos) && (inputIndex < inputSize)) {
                 // Save position for this depth, to get back to this once children are done
-                mStackChildCount[depth] = charGroupIndex;
-                mStackSiblingPos[depth] = siblingPos;
+                stackChildCount[depth] = charGroupIndex;
+                stackSiblingPos[depth] = siblingPos;
                 // Prepare stack values for next depth
                 ++depth;
                 int childrenPos = childrenNodePos;
-                mStackChildCount[depth] =
+                stackChildCount[depth] =
                         BinaryFormat::getGroupCountAndForwardPointer(root, &childrenPos);
-                mStackSiblingPos[depth] = childrenPos;
-                mStackInputIndex[depth] = inputIndex;
+                stackSiblingPos[depth] = childrenPos;
+                stackInputIndex[depth] = inputIndex;
                 pos = childrenPos;
                 // Go to the next depth level.
                 ++depth;
@@ -808,18 +833,25 @@
     return maxFreq;
 }
 
-int UnigramDictionary::getFrequency(const int32_t* const inWord, const int length) const {
-    const uint8_t* const root = DICT_ROOT;
-    int pos = BinaryFormat::getTerminalPosition(root, inWord, length);
+int UnigramDictionary::getFrequency(const int32_t *const inWord, const int length) const {
+    const uint8_t *const root = DICT_ROOT;
+    int pos = BinaryFormat::getTerminalPosition(root, inWord, length,
+            false /* forceLowerCaseSearch */);
     if (NOT_VALID_WORD == pos) {
         return NOT_A_PROBABILITY;
     }
     const uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
-    const bool hasMultipleChars = (0 != (FLAG_HAS_MULTIPLE_CHARS & flags));
+    if (flags & (BinaryFormat::FLAG_IS_BLACKLISTED | BinaryFormat::FLAG_IS_NOT_A_WORD)) {
+        // If this is not a word, or if it's a blacklisted entry, it should behave as
+        // having no frequency outside of the suggestion process (where it should be used
+        // for shortcuts).
+        return NOT_A_PROBABILITY;
+    }
+    const bool hasMultipleChars = (0 != (BinaryFormat::FLAG_HAS_MULTIPLE_CHARS & flags));
     if (hasMultipleChars) {
         pos = BinaryFormat::skipOtherCharacters(root, pos);
     } else {
-        BinaryFormat::getCharCodeAndForwardPointer(DICT_ROOT, &pos);
+        BinaryFormat::getCodePointAndForwardPointer(DICT_ROOT, &pos);
     }
     const int unigramFreq = BinaryFormat::readFrequencyWithoutMovingPointer(root, pos);
     return unigramFreq;
@@ -848,7 +880,7 @@
 inline bool UnigramDictionary::processCurrentNode(const int initialPos,
         const std::map<int, int> *bigramMap, const uint8_t *bigramFilter, Correction *correction,
         int *newCount, int *newChildrenPosition, int *nextSiblingPosition,
-        WordsPriorityQueuePool *queuePool, const int currentWordIndex) {
+        WordsPriorityQueuePool *queuePool, const int currentWordIndex) const {
     if (DEBUG_DICT) {
         correction->checkState();
     }
@@ -863,8 +895,8 @@
     // - FLAG_IS_TERMINAL: whether this node is a terminal or not (it may still have children)
     // - FLAG_HAS_BIGRAMS: whether this node has bigrams or not
     const uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(DICT_ROOT, &pos);
-    const bool hasMultipleChars = (0 != (FLAG_HAS_MULTIPLE_CHARS & flags));
-    const bool isTerminalNode = (0 != (FLAG_IS_TERMINAL & flags));
+    const bool hasMultipleChars = (0 != (BinaryFormat::FLAG_HAS_MULTIPLE_CHARS & flags));
+    const bool isTerminalNode = (0 != (BinaryFormat::FLAG_IS_TERMINAL & flags));
 
     bool needsToInvokeOnTerminal = false;
 
@@ -873,23 +905,23 @@
     // else if FLAG_IS_TERMINAL: the frequency
     // else if MASK_GROUP_ADDRESS_TYPE is not NONE: the children address
     // Note that you can't have a node that both is not a terminal and has no children.
-    int32_t c = BinaryFormat::getCharCodeAndForwardPointer(DICT_ROOT, &pos);
-    assert(NOT_A_CHARACTER != c);
+    int32_t c = BinaryFormat::getCodePointAndForwardPointer(DICT_ROOT, &pos);
+    assert(NOT_A_CODE_POINT != c);
 
     // We are going to loop through each character and make it look like it's a different
     // node each time. To do that, we will process characters in this node in order until
-    // we find the character terminator. This is signalled by getCharCode* returning
-    // NOT_A_CHARACTER.
+    // we find the character terminator. This is signalled by getCodePoint* returning
+    // NOT_A_CODE_POINT.
     // As a special case, if there is only one character in this node, we must not read the
-    // next bytes so we will simulate the NOT_A_CHARACTER return by testing the flags.
+    // next bytes so we will simulate the NOT_A_CODE_POINT return by testing the flags.
     // This way, each loop run will look like a "virtual node".
     do {
         // We prefetch the next char. If 'c' is the last char of this node, we will have
-        // NOT_A_CHARACTER in the next char. From this we can decide whether this virtual node
+        // NOT_A_CODE_POINT in the next char. From this we can decide whether this virtual node
         // should behave as a terminal or not and whether we have children.
         const int32_t nextc = hasMultipleChars
-                ? BinaryFormat::getCharCodeAndForwardPointer(DICT_ROOT, &pos) : NOT_A_CHARACTER;
-        const bool isLastChar = (NOT_A_CHARACTER == nextc);
+                ? BinaryFormat::getCodePointAndForwardPointer(DICT_ROOT, &pos) : NOT_A_CODE_POINT;
+        const bool isLastChar = (NOT_A_CODE_POINT == nextc);
         // If there are more chars in this nodes, then this virtual node is not a terminal.
         // If we are on the last char, this virtual node is a terminal if this node is.
         const bool isTerminal = isLastChar && isTerminalNode;
@@ -918,9 +950,9 @@
 
         // Prepare for the next character. Promote the prefetched char to current char - the loop
         // will take care of prefetching the next. If we finally found our last char, nextc will
-        // contain NOT_A_CHARACTER.
+        // contain NOT_A_CODE_POINT.
         c = nextc;
-    } while (NOT_A_CHARACTER != c);
+    } while (NOT_A_CODE_POINT != c);
 
     if (isTerminalNode) {
         // The frequency should be here, because we come here only if this is actually
@@ -982,5 +1014,4 @@
     *newChildrenPosition = childrenPos;
     return true;
 }
-
 } // namespace latinime
diff --git a/native/jni/src/unigram_dictionary.h b/native/jni/src/unigram_dictionary.h
index a1a8299..57129bb 100644
--- a/native/jni/src/unigram_dictionary.h
+++ b/native/jni/src/unigram_dictionary.h
@@ -19,53 +19,19 @@
 
 #include <map>
 #include <stdint.h>
-#include "correction.h"
-#include "correction_state.h"
 #include "defines.h"
-#include "proximity_info.h"
-#include "words_priority_queue.h"
-#include "words_priority_queue_pool.h"
 
 namespace latinime {
 
+class Correction;
+class ProximityInfo;
 class TerminalAttributes;
+class WordsPriorityQueuePool;
+
 class UnigramDictionary {
     typedef struct { int first; int second; int replacement; } digraph_t;
 
  public:
-    // Mask and flags for children address type selection.
-    static const int MASK_GROUP_ADDRESS_TYPE = 0xC0;
-    static const int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
-    static const int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
-    static const int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
-    static const int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
-
-    // Flag for single/multiple char group
-    static const int FLAG_HAS_MULTIPLE_CHARS = 0x20;
-
-    // Flag for terminal groups
-    static const int FLAG_IS_TERMINAL = 0x10;
-
-    // Flag for shortcut targets presence
-    static const int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
-    // Flag for bigram presence
-    static const int FLAG_HAS_BIGRAMS = 0x04;
-
-    // Attribute (bigram/shortcut) related flags:
-    // Flag for presence of more attributes
-    static const int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
-    // Flag for sign of offset. If this flag is set, the offset value must be negated.
-    static const int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
-
-    // Mask for attribute frequency, stored on 4 bits inside the flags byte.
-    static const int MASK_ATTRIBUTE_FREQUENCY = 0x0F;
-
-    // Mask and flags for attribute address type selection.
-    static const int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
-    static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
-    static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
-    static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
-
     // Error tolerances
     static const int DEFAULT_MAX_ERRORS = 2;
     static const int MAX_ERRORS_FOR_TWO_WORDS = 1;
@@ -73,80 +39,80 @@
     static const int FLAG_MULTIPLE_SUGGEST_ABORT = 0;
     static const int FLAG_MULTIPLE_SUGGEST_SKIP = 1;
     static const int FLAG_MULTIPLE_SUGGEST_CONTINUE = 2;
-    UnigramDictionary(const uint8_t* const streamStart, int typedLetterMultipler,
+    UnigramDictionary(const uint8_t *const streamStart, int typedLetterMultipler,
             int fullWordMultiplier, int maxWordLength, int maxWords, const unsigned int flags);
-    int getFrequency(const int32_t* const inWord, const int length) const;
+    int getFrequency(const int32_t *const inWord, const int length) const;
     int getBigramPosition(int pos, unsigned short *word, int offset, int length) const;
-    int getSuggestions(ProximityInfo *proximityInfo, WordsPriorityQueuePool *queuePool,
-            Correction *correction, const int *xcoordinates, const int *ycoordinates,
-            const int *codes, const int codesSize, const std::map<int, int> *bigramMap,
-            const uint8_t *bigramFilter, const bool useFullEditDistance, unsigned short *outWords,
-            int *frequencies);
+    int getSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
+            const int *ycoordinates, const int *codes, const int codesSize,
+            const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
+            const bool useFullEditDistance, unsigned short *outWords, int *frequencies,
+            int *outputTypes) const;
     virtual ~UnigramDictionary();
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(UnigramDictionary);
     void getWordSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
-            const int *ycoordinates, const int *codes, const int inputLength,
+            const 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);
+            WordsPriorityQueuePool *queuePool) const;
     int getDigraphReplacement(const int *codes, const int i, const int codesSize,
-            const digraph_t* const digraphs, const unsigned int digraphsSize) const;
+            const digraph_t *const digraphs, const unsigned int digraphsSize) const;
     void getWordWithDigraphSuggestionsRec(ProximityInfo *proximityInfo,
-        const int *xcoordinates, const int* ycoordinates, const int *codesBuffer,
+        const int *xcoordinates, const int *ycoordinates, const int *codesBuffer,
         int *xCoordinatesBuffer, int *yCoordinatesBuffer, const int codesBufferSize,
         const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
-        const bool useFullEditDistance, const int* codesSrc, const int codesRemain,
-        const int currentDepth, int* codesDest, Correction *correction,
-        WordsPriorityQueuePool* queuePool, const digraph_t* const digraphs,
-        const unsigned int digraphsSize);
+        const bool useFullEditDistance, const int *codesSrc, const int codesRemain,
+        const int currentDepth, int *codesDest, Correction *correction,
+        WordsPriorityQueuePool *queuePool, const digraph_t *const digraphs,
+        const unsigned int digraphsSize) const;
     void initSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
-            const int *ycoordinates, const int *codes, const int codesSize, Correction *correction);
+            const int *ycoordinates, const int *codes, const int codesSize,
+            Correction *correction) const;
     void getOneWordSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
             const int *ycoordinates, const int *codes, const std::map<int, int> *bigramMap,
-            const uint8_t *bigramFilter, const bool useFullEditDistance, const int inputLength,
-            Correction *correction, WordsPriorityQueuePool* queuePool);
+            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);
+            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,
-            Correction *correction, WordsPriorityQueuePool* queuePool,
-            const bool hasAutoCorrectionCandidate);
+            const bool useFullEditDistance, const int inputSize,
+            Correction *correction, WordsPriorityQueuePool *queuePool,
+            const bool hasAutoCorrectionCandidate) const;
     void onTerminal(const int freq, const TerminalAttributes& terminalAttributes,
             Correction *correction, WordsPriorityQueuePool *queuePool, const bool addToMasterQueue,
-            const int currentWordIndex);
-    bool needsToSkipCurrentNode(const unsigned short c,
-            const int inputIndex, const int skipPos, const int depth);
+            const int currentWordIndex) const;
     // Process a node by considering proximity, missing and excessive character
     bool processCurrentNode(const int initialPos, const std::map<int, int> *bigramMap,
             const uint8_t *bigramFilter, Correction *correction, int *newCount,
             int *newChildPosition, int *nextSiblingPosition, WordsPriorityQueuePool *queuePool,
-            const int currentWordIndex);
-    int getMostFrequentWordLike(const int startInputIndex, const int inputLength,
-            ProximityInfo *proximityInfo, unsigned short *word);
-    int getMostFrequentWordLikeInner(const uint16_t* const inWord, const int length,
-            short unsigned int *outWord);
+            const int currentWordIndex) const;
+    int getMostFrequentWordLike(const int startInputIndex, const int inputSize,
+            Correction *correction, unsigned short *word) const;
+    int getMostFrequentWordLikeInner(const uint16_t *const inWord, const int inputSize,
+            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);
+            int *wordLengthArray, unsigned short *outputWord, int *outputWordLength) const;
     void getMultiWordsSuggestionRec(ProximityInfo *proximityInfo,
             const int *xcoordinates, const int *ycoordinates, const int *codes,
-            const bool useFullEditDistance, const int inputLength,
-            Correction *correction, WordsPriorityQueuePool* queuePool,
+            const bool useFullEditDistance, const int inputSize,
+            Correction *correction, WordsPriorityQueuePool *queuePool,
             const bool hasAutoCorrectionCandidate, const int startPos, const int startWordIndex,
-            const int outputWordLength, int *freqArray, int* wordLengthArray,
-            unsigned short* outputWord);
+            const int outputWordLength, int *freqArray, int *wordLengthArray,
+            unsigned short *outputWord) const;
 
-    const uint8_t* const DICT_ROOT;
+    const uint8_t *const DICT_ROOT;
     const int MAX_WORD_LENGTH;
     const int MAX_WORDS;
     const int TYPED_LETTER_MULTIPLIER;
@@ -158,13 +124,6 @@
 
     static const digraph_t GERMAN_UMLAUT_DIGRAPHS[];
     static const digraph_t FRENCH_LIGATURES_DIGRAPHS[];
-
-    // Still bundled members
-    unsigned short mWord[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
-    int mStackChildCount[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
-    int mStackInputIndex[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
-    int mStackSiblingPos[MAX_WORD_LENGTH_INTERNAL];// TODO: remove
 };
 } // namespace latinime
-
 #endif // LATINIME_UNIGRAM_DICTIONARY_H
diff --git a/native/jni/src/words_priority_queue.h b/native/jni/src/words_priority_queue.h
index 7629251..19efa5d 100644
--- a/native/jni/src/words_priority_queue.h
+++ b/native/jni/src/words_priority_queue.h
@@ -18,8 +18,9 @@
 #define LATINIME_WORDS_PRIORITY_QUEUE_H
 
 #include <cstring> // for memcpy()
-#include <iostream>
 #include <queue>
+
+#include "correction.h"
 #include "defines.h"
 
 namespace latinime {
@@ -32,31 +33,32 @@
         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) {
-        SuggestedWord* sw = 0;
+    void push(int score, unsigned short *word, int wordLength, int type) {
+        SuggestedWord *sw = 0;
         if (mSuggestions.size() >= MAX_WORDS) {
             sw = mSuggestions.top();
             const int minScore = sw->mScore;
@@ -68,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.");
@@ -86,21 +88,21 @@
         }
     }
 
-    SuggestedWord* top() {
+    SuggestedWord *top() {
         if (mSuggestions.empty()) return 0;
-        SuggestedWord* sw = mSuggestions.top();
+        SuggestedWord *sw = mSuggestions.top();
         return sw;
     }
 
-    int outputSuggestions(const unsigned short* before, const int beforeLength,
-            int *frequencies, unsigned short *outputChars) {
+    int outputSuggestions(const unsigned short *before, const int beforeLength,
+            int *frequencies, unsigned short *outputChars, int* outputTypes) {
         mHighestSuggestedWord = 0;
         const unsigned int size = min(
               MAX_WORDS, static_cast<unsigned int>(mSuggestions.size()));
-        SuggestedWord* swBuffer[size];
+        SuggestedWord *swBuffer[size];
         int index = size - 1;
         while (!mSuggestions.empty() && index >= 0) {
-            SuggestedWord* sw = mSuggestions.top();
+            SuggestedWord *sw = mSuggestions.top();
             if (DEBUG_WORDS_PRIORITY_QUEUE) {
                 AKLOGI("dump word. %d", sw->mScore);
                 DUMP_WORD(sw->mWord, sw->mWordLength);
@@ -110,11 +112,11 @@
             --index;
         }
         if (size >= 2) {
-            SuggestedWord* nsMaxSw = 0;
+            SuggestedWord *nsMaxSw = 0;
             unsigned int maxIndex = 0;
             float maxNs = 0;
             for (unsigned int i = 0; i < size; ++i) {
-                SuggestedWord* tempSw = swBuffer[i];
+                SuggestedWord *tempSw = swBuffer[i];
                 if (!tempSw) {
                     continue;
                 }
@@ -126,22 +128,23 @@
                 }
             }
             if (maxIndex > 0 && nsMaxSw) {
-                memmove(&swBuffer[1], &swBuffer[0], maxIndex * sizeof(SuggestedWord*));
+                memmove(&swBuffer[1], &swBuffer[0], maxIndex * sizeof(SuggestedWord *));
                 swBuffer[0] = nsMaxSw;
             }
         }
         for (unsigned int i = 0; i < size; ++i) {
-            SuggestedWord* sw = swBuffer[i];
+            SuggestedWord *sw = swBuffer[i];
             if (!sw) {
                 AKLOGE("SuggestedWord is null %d", i);
                 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;
         }
@@ -155,7 +158,7 @@
     void clear() {
         mHighestSuggestedWord = 0;
         while (!mSuggestions.empty()) {
-            SuggestedWord* sw = mSuggestions.top();
+            SuggestedWord *sw = mSuggestions.top();
             if (DEBUG_WORDS_PRIORITY_QUEUE) {
                 AKLOGI("Clear word. %d", sw->mScore);
                 DUMP_WORD(sw->mWord, sw->mWordLength);
@@ -172,8 +175,8 @@
         DUMP_WORD(mHighestSuggestedWord->mWord, mHighestSuggestedWord->mWordLength);
     }
 
-    float getHighestNormalizedScore(const unsigned short* before, const int beforeLength,
-            unsigned short** outWord, int *outScore, int *outLength) {
+    float getHighestNormalizedScore(const unsigned short *before, const int beforeLength,
+            unsigned short **outWord, int *outScore, int *outLength) {
         if (!mHighestSuggestedWord) {
             return 0.0;
         }
@@ -182,27 +185,28 @@
     }
 
  private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(WordsPriorityQueue);
     struct wordComparator {
         bool operator ()(SuggestedWord * left, SuggestedWord * right) {
             return left->mScore > right->mScore;
         }
     };
 
-    SuggestedWord* getFreeSuggestedWord(int score, unsigned short* word,
-            int wordLength) {
+    SuggestedWord *getFreeSuggestedWord(int score, unsigned short *word,
+            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];
             }
         }
         return 0;
     }
 
-    static float getNormalizedScore(SuggestedWord* sw, const unsigned short* before,
-            const int beforeLength, unsigned short** outWord, int *outScore, int *outLength) {
+    static float getNormalizedScore(SuggestedWord *sw, const unsigned short *before,
+            const int beforeLength, unsigned short **outWord, int *outScore, int *outLength) {
         const int score = sw->mScore;
-        unsigned short* word = sw->mWord;
+        unsigned short *word = sw->mWord;
         const int wordLength = sw->mWordLength;
         if (outScore) {
             *outScore = score;
@@ -217,14 +221,13 @@
                 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;
     const unsigned int MAX_WORD_LENGTH;
-    SuggestedWord* mSuggestedWords;
-    SuggestedWord* mHighestSuggestedWord;
+    SuggestedWord *mSuggestedWords;
+    SuggestedWord *mHighestSuggestedWord;
 };
-}
-
+} // namespace latinime
 #endif // LATINIME_WORDS_PRIORITY_QUEUE_H
diff --git a/native/jni/src/words_priority_queue_pool.h b/native/jni/src/words_priority_queue_pool.h
index 210b5a8..2d52903 100644
--- a/native/jni/src/words_priority_queue_pool.h
+++ b/native/jni/src/words_priority_queue_pool.h
@@ -17,20 +17,20 @@
 #ifndef LATINIME_WORDS_PRIORITY_QUEUE_POOL_H
 #define LATINIME_WORDS_PRIORITY_QUEUE_POOL_H
 
-#include <assert.h>
-#include <new>
+#include <cassert>
 #include "words_priority_queue.h"
 
 namespace latinime {
 
 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)) {
+                ++i, subQueueBufOffset += static_cast<int>(sizeof(WordsPriorityQueue))) {
             mSubQueues[i] = new(mSubQueueBuf + subQueueBufOffset)
                     WordsPriorityQueue(subQueueMaxWords, maxWordLength);
         }
@@ -44,11 +44,11 @@
         }
     }
 
-    WordsPriorityQueue* getMasterQueue() {
+    WordsPriorityQueue *getMasterQueue() {
         return mMasterQueue;
     }
 
-    WordsPriorityQueue* getSubQueue(const int wordIndex, const int inputWordLength) {
+    WordsPriorityQueue *getSubQueue(const int wordIndex, const int inputWordLength) {
         if (wordIndex >= MULTIPLE_WORDS_SUGGESTION_MAX_WORDS) {
             return 0;
         }
@@ -70,7 +70,7 @@
 
     inline void clearSubQueue(const int wordIndex) {
         for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
-            WordsPriorityQueue* queue = getSubQueue(wordIndex, i);
+            WordsPriorityQueue *queue = getSubQueue(wordIndex, i);
             if (queue) {
                 queue->clear();
             }
@@ -85,12 +85,12 @@
     }
 
  private:
-    WordsPriorityQueue* mMasterQueue;
-    WordsPriorityQueue* mSubQueues[SUB_QUEUE_MAX_COUNT * MULTIPLE_WORDS_SUGGESTION_MAX_WORDS];
+    DISALLOW_IMPLICIT_CONSTRUCTORS(WordsPriorityQueuePool);
     char mMasterQueueBuf[sizeof(WordsPriorityQueue)];
-    char mSubQueueBuf[MULTIPLE_WORDS_SUGGESTION_MAX_WORDS
-                      * SUB_QUEUE_MAX_COUNT * 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];
 };
-}
-
+} // namespace latinime
 #endif // LATINIME_WORDS_PRIORITY_QUEUE_POOL_H
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 38a2ecf..6bd46cc 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.inputmethod.latin.tests">
 
+    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="16" />
+
     <uses-permission android:name="android.permission.READ_CONTACTS" />
 
     <application>
diff --git a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
index 5c6c834..2a244a7 100644
--- a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
@@ -18,7 +18,7 @@
 
 import android.test.AndroidTestCase;
 
-import com.android.inputmethod.keyboard.MoreKeysKeyboard.Builder.MoreKeysKeyboardParams;
+import com.android.inputmethod.keyboard.MoreKeysKeyboard.MoreKeysKeyboardParams;
 
 public class MoreKeysKeyboardBuilderFixedOrderTests extends AndroidTestCase {
     private static final int WIDTH = 10;
diff --git a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
index 31f0e0f..e6c76db 100644
--- a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
@@ -18,7 +18,7 @@
 
 import android.test.AndroidTestCase;
 
-import com.android.inputmethod.keyboard.MoreKeysKeyboard.Builder.MoreKeysKeyboardParams;
+import com.android.inputmethod.keyboard.MoreKeysKeyboard.MoreKeysKeyboardParams;
 
 public class MoreKeysKeyboardBuilderTests extends AndroidTestCase {
     private static final int WIDTH = 10;
diff --git a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
index a34e0ef..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;
 
@@ -46,7 +47,7 @@
     public void testAllFullDisplayName() {
         for (final InputMethodSubtype subtype : mSubtypesList) {
             final String subtypeName = SubtypeLocale.getSubtypeDisplayName(subtype, mRes);
-            final String spacebarText = LatinKeyboardView.getFullDisplayName(subtype, mRes);
+            final String spacebarText = MainKeyboardView.getFullDisplayName(subtype, mRes);
             final String languageName =
                     SubtypeLocale.getSubtypeLocaleDisplayName(subtype.getLocale());
             if (SubtypeLocale.isNoLanguage(subtype)) {
@@ -60,7 +61,7 @@
    public void testAllMiddleDisplayName() {
         for (final InputMethodSubtype subtype : mSubtypesList) {
             final String subtypeName = SubtypeLocale.getSubtypeDisplayName(subtype, mRes);
-            final String spacebarText = LatinKeyboardView.getMiddleDisplayName(subtype);
+            final String spacebarText = MainKeyboardView.getMiddleDisplayName(subtype);
             if (SubtypeLocale.isNoLanguage(subtype)) {
                 assertEquals(subtypeName,
                         SubtypeLocale.getKeyboardLayoutSetName(subtype), spacebarText);
@@ -76,7 +77,7 @@
         for (final InputMethodSubtype subtype : mSubtypesList) {
             final String subtypeName = SubtypeLocale.getSubtypeDisplayName(subtype, mRes);
             final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
-            final String spacebarText = LatinKeyboardView.getShortDisplayName(subtype);
+            final String spacebarText = MainKeyboardView.getShortDisplayName(subtype);
             final String languageCode = StringUtils.toTitleCase(locale.getLanguage(), locale);
             if (SubtypeLocale.isNoLanguage(subtype)) {
                 assertEquals(subtypeName, "", spacebarText);
@@ -117,31 +118,31 @@
                 context, SubtypeLocale.NO_LANGUAGE, "qwerty");
 
         assertEquals("en_US", "English (US)",
-                LatinKeyboardView.getFullDisplayName(EN_US, mRes));
+                MainKeyboardView.getFullDisplayName(EN_US, mRes));
         assertEquals("en_GB", "English (UK)",
-                LatinKeyboardView.getFullDisplayName(EN_GB, mRes));
+                MainKeyboardView.getFullDisplayName(EN_GB, mRes));
         assertEquals("fr   ", "Français",
-                LatinKeyboardView.getFullDisplayName(FR, mRes));
+                MainKeyboardView.getFullDisplayName(FR, mRes));
         assertEquals("fr_CA", "Français (Canada)",
-                LatinKeyboardView.getFullDisplayName(FR_CA, mRes));
+                MainKeyboardView.getFullDisplayName(FR_CA, mRes));
         assertEquals("de   ", "Deutsch",
-                LatinKeyboardView.getFullDisplayName(DE, mRes));
+                MainKeyboardView.getFullDisplayName(DE, mRes));
         assertEquals("zz   ", "QWERTY",
-                LatinKeyboardView.getFullDisplayName(ZZ, mRes));
+                MainKeyboardView.getFullDisplayName(ZZ, mRes));
 
-        assertEquals("en_US", "English",  LatinKeyboardView.getMiddleDisplayName(EN_US));
-        assertEquals("en_GB", "English",  LatinKeyboardView.getMiddleDisplayName(EN_GB));
-        assertEquals("fr   ", "Français", LatinKeyboardView.getMiddleDisplayName(FR));
-        assertEquals("fr_CA", "Français", LatinKeyboardView.getMiddleDisplayName(FR_CA));
-        assertEquals("de   ", "Deutsch",  LatinKeyboardView.getMiddleDisplayName(DE));
-        assertEquals("zz   ", "QWERTY",   LatinKeyboardView.getMiddleDisplayName(ZZ));
+        assertEquals("en_US", "English",  MainKeyboardView.getMiddleDisplayName(EN_US));
+        assertEquals("en_GB", "English",  MainKeyboardView.getMiddleDisplayName(EN_GB));
+        assertEquals("fr   ", "Français", MainKeyboardView.getMiddleDisplayName(FR));
+        assertEquals("fr_CA", "Français", MainKeyboardView.getMiddleDisplayName(FR_CA));
+        assertEquals("de   ", "Deutsch",  MainKeyboardView.getMiddleDisplayName(DE));
+        assertEquals("zz   ", "QWERTY",   MainKeyboardView.getMiddleDisplayName(ZZ));
 
-        assertEquals("en_US", "En", LatinKeyboardView.getShortDisplayName(EN_US));
-        assertEquals("en_GB", "En", LatinKeyboardView.getShortDisplayName(EN_GB));
-        assertEquals("fr   ", "Fr", LatinKeyboardView.getShortDisplayName(FR));
-        assertEquals("fr_CA", "Fr", LatinKeyboardView.getShortDisplayName(FR_CA));
-        assertEquals("de   ", "De", LatinKeyboardView.getShortDisplayName(DE));
-        assertEquals("zz   ", "",   LatinKeyboardView.getShortDisplayName(ZZ));
+        assertEquals("en_US", "En", MainKeyboardView.getShortDisplayName(EN_US));
+        assertEquals("en_GB", "En", MainKeyboardView.getShortDisplayName(EN_GB));
+        assertEquals("fr   ", "Fr", MainKeyboardView.getShortDisplayName(FR));
+        assertEquals("fr_CA", "Fr", MainKeyboardView.getShortDisplayName(FR_CA));
+        assertEquals("de   ", "De", MainKeyboardView.getShortDisplayName(DE));
+        assertEquals("zz   ", "",   MainKeyboardView.getShortDisplayName(ZZ));
     }
 
     public void testAdditionalSubtype() {
@@ -155,22 +156,22 @@
                 SubtypeLocale.NO_LANGUAGE, "azerty", null);
 
         assertEquals("fr qwertz",    "Français (QWERTZ)",
-                LatinKeyboardView.getFullDisplayName(FR_QWERTZ, mRes));
+                MainKeyboardView.getFullDisplayName(FR_QWERTZ, mRes));
         assertEquals("de qwerty",    "Deutsch (QWERTY)",
-                LatinKeyboardView.getFullDisplayName(DE_QWERTY, mRes));
+                MainKeyboardView.getFullDisplayName(DE_QWERTY, mRes));
         assertEquals("en_US azerty", "English (US) (AZERTY)",
-                LatinKeyboardView.getFullDisplayName(US_AZERTY, mRes));
+                MainKeyboardView.getFullDisplayName(US_AZERTY, mRes));
         assertEquals("zz azerty",    "AZERTY",
-                LatinKeyboardView.getFullDisplayName(ZZ_AZERTY, mRes));
+                MainKeyboardView.getFullDisplayName(ZZ_AZERTY, mRes));
 
-        assertEquals("fr qwertz",    "Français", LatinKeyboardView.getMiddleDisplayName(FR_QWERTZ));
-        assertEquals("de qwerty",    "Deutsch",  LatinKeyboardView.getMiddleDisplayName(DE_QWERTY));
-        assertEquals("en_US azerty", "English",  LatinKeyboardView.getMiddleDisplayName(US_AZERTY));
-        assertEquals("zz azerty",    "AZERTY",   LatinKeyboardView.getMiddleDisplayName(ZZ_AZERTY));
+        assertEquals("fr qwertz",    "Français", MainKeyboardView.getMiddleDisplayName(FR_QWERTZ));
+        assertEquals("de qwerty",    "Deutsch",  MainKeyboardView.getMiddleDisplayName(DE_QWERTY));
+        assertEquals("en_US azerty", "English",  MainKeyboardView.getMiddleDisplayName(US_AZERTY));
+        assertEquals("zz azerty",    "AZERTY",   MainKeyboardView.getMiddleDisplayName(ZZ_AZERTY));
 
-        assertEquals("fr qwertz",    "Fr", LatinKeyboardView.getShortDisplayName(FR_QWERTZ));
-        assertEquals("de qwerty",    "De", LatinKeyboardView.getShortDisplayName(DE_QWERTY));
-        assertEquals("en_US azerty", "En", LatinKeyboardView.getShortDisplayName(US_AZERTY));
-        assertEquals("zz azerty",    "",  LatinKeyboardView.getShortDisplayName(ZZ_AZERTY));
+        assertEquals("fr qwertz",    "Fr", MainKeyboardView.getShortDisplayName(FR_QWERTZ));
+        assertEquals("de qwerty",    "De", MainKeyboardView.getShortDisplayName(DE_QWERTY));
+        assertEquals("en_US azerty", "En", MainKeyboardView.getShortDisplayName(US_AZERTY));
+        assertEquals("zz azerty",    "",  MainKeyboardView.getShortDisplayName(ZZ_AZERTY));
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
index fa067f4..1346c00 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
@@ -16,31 +16,35 @@
 
 package com.android.inputmethod.keyboard.internal;
 
-import android.test.AndroidTestCase;
+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;
 import java.util.Locale;
 
-public class KeySpecParserCsvTests extends AndroidTestCase {
+public class KeySpecParserCsvTests extends InstrumentationTestCase {
     private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
 
+        final Instrumentation instrumentation = getInstrumentation();
         mTextsSet.setLanguage(Locale.ENGLISH.getLanguage());
-        mTextsSet.loadStringResources(getContext());
+        mTextsSet.loadStringResources(instrumentation.getTargetContext());
         final String[] testResourceNames = getAllResourceIdNames(
                 com.android.inputmethod.latin.tests.R.string.class);
-        mTextsSet.loadStringResourcesInternal(getTestContext(),
+        mTextsSet.loadStringResourcesInternal(instrumentation.getContext(),
                 testResourceNames,
                 com.android.inputmethod.latin.tests.R.string.empty_string);
     }
 
     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/KeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
index 0b174a7..1ab5775 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
@@ -23,7 +23,6 @@
 import android.test.AndroidTestCase;
 
 import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec;
 
 import java.util.Arrays;
 import java.util.Locale;
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
index 64cf7a6..f5ad723 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
@@ -420,38 +420,38 @@
 
     public void testDoubleTapShiftAndChording() {
         // TODO: The following tests fail due to bug. Temporarily commented.
-        // First shift key tap.
-        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
-        // Second shift key tap, maybe shift locked.
-        secondPressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
-        // Press/release letter key, remain in manual shifted.
-        chordingPressAndReleaseKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
-        // Release shift key, back to alphabet shifted (not shift locked).
-        releaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
-
-        // Long press shift key, enter alphabet shift locked.
-        longPressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED,
-                ALPHABET_SHIFT_LOCKED);
-        // First shift key tap.
-        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
-        // Second shift key tap, maybe shift unlocked.
-        secondPressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
-        // Press/release letter key, remain in manual shifted.
-        chordingPressAndReleaseKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
-        // Release shift key, back to alphabet (not shift locked).
-        releaseKey(CODE_SHIFT, ALPHABET_UNSHIFTED);
-
-        // Set capitalize the first character of all words mode.
-        setAutoCapsMode(CAP_MODE_WORDS);
-        // Load keyboard, should be in automatic shifted.
-        loadKeyboard(ALPHABET_AUTOMATIC_SHIFTED);
-        // First shift key tap.
-        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
-        // Second shift key tap, maybe shift locked.
-        secondPressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
-        // Press/release letter key, remain in manual shifted.
-        chordingPressAndReleaseKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
-        // Release shift key, back to alphabet (not shift locked).
-        releaseKey(CODE_SHIFT, ALPHABET_UNSHIFTED);
+//        // First shift key tap.
+//        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+//        // Second shift key tap, maybe shift locked.
+//        secondPressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
+//        // Press/release letter key, remain in manual shifted.
+//        chordingPressAndReleaseKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+//        // Release shift key, back to alphabet shifted (not shift locked).
+//        releaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
+//
+//        // Long press shift key, enter alphabet shift locked.
+//        longPressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED,
+//                ALPHABET_SHIFT_LOCKED);
+//        // First shift key tap.
+//        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+//        // Second shift key tap, maybe shift unlocked.
+//        secondPressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
+//        // Press/release letter key, remain in manual shifted.
+//        chordingPressAndReleaseKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+//        // Release shift key, back to alphabet (not shift locked).
+//        releaseKey(CODE_SHIFT, ALPHABET_UNSHIFTED);
+//
+//        // Set capitalize the first character of all words mode.
+//        setAutoCapsMode(CAP_MODE_WORDS);
+//        // Load keyboard, should be in automatic shifted.
+//        loadKeyboard(ALPHABET_AUTOMATIC_SHIFTED);
+//        // First shift key tap.
+//        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+//        // Second shift key tap, maybe shift locked.
+//        secondPressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
+//        // Press/release letter key, remain in manual shifted.
+//        chordingPressAndReleaseKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+//        // Release shift key, back to alphabet (not shift locked).
+//        releaseKey(CODE_SHIFT, ALPHABET_UNSHIFTED);
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
new file mode 100644
index 0000000..8fed28f
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
@@ -0,0 +1,316 @@
+/*
+ * 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.test.AndroidTestCase;
+
+public class PointerTrackerQueueTests extends AndroidTestCase {
+    public static class Element implements PointerTrackerQueue.Element {
+        public static int sPhantomUpCount;
+        public static final long NOT_HAPPENED = -1;
+
+        public final int mId;
+        public boolean mIsModifier;
+        public boolean mIsInSlidingKeyInput;
+        public long mPhantomUpEventTime = NOT_HAPPENED;
+
+        public Element(int id) {
+            mId = id;
+        }
+
+        @Override
+        public boolean isModifier() {
+            return mIsModifier;
+        }
+
+        @Override
+        public boolean isInSlidingKeyInput() {
+            return mIsInSlidingKeyInput;
+        }
+
+        @Override
+        public void onPhantomUpEvent(long eventTime) {
+            sPhantomUpCount++;
+            mPhantomUpEventTime = eventTime + sPhantomUpCount;
+        }
+
+        @Override
+        public String toString() {
+            return Integer.toString(mId);
+        }
+    }
+
+    private final Element mElement1 = new Element(1);
+    private final Element mElement2 = new Element(2);
+    private final Element mElement3 = new Element(3);
+    private final Element mElement4 = new Element(4);
+    private final PointerTrackerQueue mQueue = new PointerTrackerQueue();
+
+    public void testEmpty() {
+        assertEquals("empty queue", 0, mQueue.size());
+        assertEquals("empty queue", "[]", mQueue.toString());
+    }
+
+    public void testAdd() {
+        mQueue.add(mElement1);
+        assertEquals("add element1", 1, mQueue.size());
+        assertEquals("after adding element1", "[1]", mQueue.toString());
+        mQueue.add(mElement2);
+        assertEquals("add element2", 2, mQueue.size());
+        assertEquals("after adding element2", "[1 2]", mQueue.toString());
+        mQueue.add(mElement3);
+        assertEquals("add element3", 3, mQueue.size());
+        assertEquals("after adding element3", "[1 2 3]", mQueue.toString());
+        mQueue.add(mElement4);
+        assertEquals("add element4", 4, mQueue.size());
+        assertEquals("after adding element4", "[1 2 3 4]", mQueue.toString());
+    }
+
+    public void testRemove() {
+        Element.sPhantomUpCount = 0;
+
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        mQueue.remove(mElement2);
+        assertEquals("remove element2", 3, mQueue.size());
+        assertEquals("after removing element2", "[1 3 4]", mQueue.toString());
+        mQueue.remove(mElement4);
+        assertEquals("remove element4", 2, mQueue.size());
+        assertEquals("after removing element4", "[1 3]", mQueue.toString());
+        mQueue.remove(mElement4);
+        assertEquals("remove element4 again", 2, mQueue.size());
+        assertEquals("after removing element4 again", "[1 3]", mQueue.toString());
+        mQueue.remove(mElement1);
+        assertEquals("remove element1", 1, mQueue.size());
+        assertEquals("after removing element4", "[3]", mQueue.toString());
+        mQueue.remove(mElement3);
+        assertEquals("remove element3", 0, mQueue.size());
+        assertEquals("after removing element3", "[]", mQueue.toString());
+        mQueue.remove(mElement1);
+        assertEquals("remove element1 again", 0, mQueue.size());
+        assertEquals("after removing element1 again", "[]", mQueue.toString());
+
+        assertEquals("after remove elements", 0, Element.sPhantomUpCount);
+        assertEquals("after remove element1",
+                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals("after remove element2",
+                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals("after remove element3",
+                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals("after remove element4",
+                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testAddAndRemove() {
+        Element.sPhantomUpCount = 0;
+
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        mQueue.remove(mElement2);
+        assertEquals("remove element2", 3, mQueue.size());
+        assertEquals("after removing element2", "[1 3 4]", mQueue.toString());
+        mQueue.remove(mElement4);
+        assertEquals("remove element4", 2, mQueue.size());
+        assertEquals("after removing element4", "[1 3]", mQueue.toString());
+        mQueue.add(mElement2);
+        assertEquals("add element2", 3, mQueue.size());
+        assertEquals("after adding element2", "[1 3 2]", mQueue.toString());
+        mQueue.remove(mElement4);
+        assertEquals("remove element4 again", 3, mQueue.size());
+        assertEquals("after removing element4 again", "[1 3 2]", mQueue.toString());
+        mQueue.remove(mElement1);
+        assertEquals("remove element1", 2, mQueue.size());
+        assertEquals("after removing element4", "[3 2]", mQueue.toString());
+        mQueue.add(mElement1);
+        assertEquals("add element1", 3, mQueue.size());
+        assertEquals("after adding element1", "[3 2 1]", mQueue.toString());
+        mQueue.remove(mElement3);
+        assertEquals("remove element3", 2, mQueue.size());
+        assertEquals("after removing element3", "[2 1]", mQueue.toString());
+        mQueue.remove(mElement1);
+        assertEquals("remove element1 again", 1, mQueue.size());
+        assertEquals("after removing element1 again", "[2]", mQueue.toString());
+
+        assertEquals("after remove element1",
+                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals("after remove element2",
+                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals("after remove element3",
+                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals("after remove element4",
+                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testReleaseAllPointers() {
+        mElement2.mIsModifier = true;
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        final long eventTime = 123;
+        Element.sPhantomUpCount = 0;
+        mQueue.releaseAllPointers(eventTime);
+        assertEquals("after releaseAllPointers", 4, Element.sPhantomUpCount);
+        assertEquals("after releaseAllPointers", 0, mQueue.size());
+        assertEquals("after releaseAllPointers", "[]", mQueue.toString());
+        assertEquals("after releaseAllPointers element1",
+                eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointers element2",
+                eventTime + 2, mElement2.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointers element3",
+                eventTime + 3, mElement3.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointers element4",
+                eventTime + 4, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testReleaseAllPointersOlderThan() {
+        mElement2.mIsModifier = true;
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        final long eventTime = 123;
+        Element.sPhantomUpCount = 0;
+        mQueue.releaseAllPointersOlderThan(mElement4, eventTime);
+        assertEquals("after releaseAllPointersOlderThan", 2, Element.sPhantomUpCount);
+        assertEquals("after releaseAllPointersOlderThan", 2, mQueue.size());
+        assertEquals("after releaseAllPointersOlderThan", "[2 4]", mQueue.toString());
+        assertEquals("after releaseAllPointersOlderThan element1",
+                eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersOlderThan element2",
+                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersOlderThan element3",
+                eventTime + 2, mElement3.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersOlderThan element4",
+                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testReleaseAllPointersOlderThanWithoutModifier() {
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        final long eventTime = 123;
+        Element.sPhantomUpCount = 0;
+        mQueue.releaseAllPointersOlderThan(mElement4, eventTime);
+        assertEquals("after releaseAllPointersOlderThan without modifier",
+                3, Element.sPhantomUpCount);
+        assertEquals("after releaseAllPointersOlderThan without modifier", 1, mQueue.size());
+        assertEquals("after releaseAllPointersOlderThan without modifier",
+                "[4]", mQueue.toString());
+        assertEquals("after releaseAllPointersOlderThan without modifier element1",
+                eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersOlderThan without modifier element2",
+                eventTime + 2, mElement2.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersOlderThan without modifier element3",
+                eventTime + 3, mElement3.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersOlderThan without modifier element4",
+                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testReleaseAllPointersExcept() {
+        mElement2.mIsModifier = true;
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        final long eventTime = 123;
+        Element.sPhantomUpCount = 0;
+        mQueue.releaseAllPointersExcept(mElement3, eventTime);
+        assertEquals("after releaseAllPointersExcept", 3, Element.sPhantomUpCount);
+        assertEquals("after releaseAllPointersExcept", 1, mQueue.size());
+        assertEquals("after releaseAllPointersExcept", "[3]", mQueue.toString());
+        assertEquals("after releaseAllPointersExcept element1",
+                eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersExcept element2",
+                eventTime + 2, mElement2.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersExcept element3",
+                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals("after releaseAllPointersExcept element4",
+                eventTime + 3, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testHasModifierKeyOlderThan() {
+        Element.sPhantomUpCount = 0;
+        assertFalse("hasModifierKeyOlderThan empty", mQueue.hasModifierKeyOlderThan(mElement1));
+
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        assertFalse("hasModifierKeyOlderThan element1", mQueue.hasModifierKeyOlderThan(mElement1));
+        assertFalse("hasModifierKeyOlderThan element2", mQueue.hasModifierKeyOlderThan(mElement2));
+        assertFalse("hasModifierKeyOlderThan element3", mQueue.hasModifierKeyOlderThan(mElement3));
+        assertFalse("hasModifierKeyOlderThan element4", mQueue.hasModifierKeyOlderThan(mElement4));
+
+        mElement2.mIsModifier = true;
+        assertFalse("hasModifierKeyOlderThan element1", mQueue.hasModifierKeyOlderThan(mElement1));
+        assertFalse("hasModifierKeyOlderThan element2", mQueue.hasModifierKeyOlderThan(mElement2));
+        assertTrue("hasModifierKeyOlderThan element3", mQueue.hasModifierKeyOlderThan(mElement3));
+        assertTrue("hasModifierKeyOlderThan element4", mQueue.hasModifierKeyOlderThan(mElement4));
+
+        assertEquals("after hasModifierKeyOlderThan", 0, Element.sPhantomUpCount);
+        assertEquals("after hasModifierKeyOlderThan", 4, mQueue.size());
+        assertEquals("after hasModifierKeyOlderThan", "[1 2 3 4]", mQueue.toString());
+        assertEquals("after hasModifierKeyOlderThan element1",
+                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals("after hasModifierKeyOlderThan element2",
+                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals("after hasModifierKeyOlderThan element3",
+                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals("after hasModifierKeyOlderThan element4",
+                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testIsAnyInSlidingKeyInput() {
+        Element.sPhantomUpCount = 0;
+        assertFalse("isAnyInSlidingKeyInput empty", mQueue.isAnyInSlidingKeyInput());
+
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        assertFalse("isAnyInSlidingKeyInput element1", mQueue.isAnyInSlidingKeyInput());
+
+        mElement3.mIsInSlidingKeyInput = true;
+        assertTrue("isAnyInSlidingKeyInput element1", mQueue.isAnyInSlidingKeyInput());
+
+        assertEquals("after isAnyInSlidingKeyInput", 0, Element.sPhantomUpCount);
+        assertEquals("after isAnyInSlidingKeyInput", 4, mQueue.size());
+        assertEquals("after isAnyInSlidingKeyInput", "[1 2 3 4]", mQueue.toString());
+        assertEquals("after isAnyInSlidingKeyInput element1",
+                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals("after isAnyInSlidingKeyInput element2",
+                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals("after isAnyInSlidingKeyInput element3",
+                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals("after isAnyInSlidingKeyInput element4",
+                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+    }
+}
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..123959b
--- /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, false /* isNotAWord */);
+        assertNull(FusionDictionary.findWordInTree(dict.mRoot, "aaa"));
+        assertNotNull(FusionDictionary.findWordInTree(dict.mRoot, "abc"));
+
+        dict.add("aa", 10, null, false /* isNotAWord */);
+        assertNull(FusionDictionary.findWordInTree(dict.mRoot, "aaa"));
+        assertNotNull(FusionDictionary.findWordInTree(dict.mRoot, "aa"));
+
+        dict.add("babcd", 10, null, false /* isNotAWord */);
+        dict.add("bacde", 10, null, false /* isNotAWord */);
+        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/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index f1ccfdd..38e57aa 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -126,6 +126,26 @@
                 mTextView.getText().toString());
     }
 
+    public void testAutoCorrectWithSpaceThenRevert() {
+        final String STRING_TO_TYPE = "tgis ";
+        final String EXPECTED_RESULT = "tgis ";
+        type(STRING_TO_TYPE);
+        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
+        type(Keyboard.CODE_DELETE);
+        assertEquals("auto-correct with space then revert", EXPECTED_RESULT,
+                mTextView.getText().toString());
+    }
+
+    public void testAutoCorrectToSelfDoesNotRevert() {
+        final String STRING_TO_TYPE = "this ";
+        final String EXPECTED_RESULT = "this";
+        type(STRING_TO_TYPE);
+        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
+        type(Keyboard.CODE_DELETE);
+        assertEquals("auto-correct with space does not revert", EXPECTED_RESULT,
+                mTextView.getText().toString());
+    }
+
     public void testDoubleSpace() {
         final String STRING_TO_TYPE = "this  ";
         final String EXPECTED_RESULT = "this. ";
@@ -196,6 +216,19 @@
         assertEquals("manual pick then separator", EXPECTED_RESULT, mTextView.getText().toString());
     }
 
+    public void testManualPickThenStripperThenPick() {
+        final String WORD_TO_TYPE = "this";
+        final String STRIPPER = "\n";
+        final String EXPECTED_RESULT = "this\nthis";
+        type(WORD_TO_TYPE);
+        pickSuggestionManually(0, WORD_TO_TYPE);
+        type(STRIPPER);
+        type(WORD_TO_TYPE);
+        pickSuggestionManually(0, WORD_TO_TYPE);
+        assertEquals("manual pick then \\n then manual pick", EXPECTED_RESULT,
+                mTextView.getText().toString());
+    }
+
     public void testManualPickThenSpaceThenType() {
         final String WORD1_TO_TYPE = "this";
         final String WORD2_TO_TYPE = " is";
diff --git a/tests/src/com/android/inputmethod/latin/InputPointersTests.java b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
new file mode 100644
index 0000000..cc55076
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
@@ -0,0 +1,235 @@
+/*
+ * 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 java.util.Arrays;
+
+public class InputPointersTests extends AndroidTestCase {
+    private static final int DEFAULT_CAPACITY = 48;
+
+    public void testNewInstance() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        assertEquals("new instance size", 0, src.getPointerSize());
+        assertNotNull("new instance xCoordinates", src.getXCoordinates());
+        assertNotNull("new instance yCoordinates", src.getYCoordinates());
+        assertNotNull("new instance pointerIds", src.getPointerIds());
+        assertNotNull("new instance times", src.getTimes());
+    }
+
+    public void testReset() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        final int[] xCoordinates = src.getXCoordinates();
+        final int[] yCoordinates = src.getXCoordinates();
+        final int[] pointerIds = src.getXCoordinates();
+        final int[] times = src.getXCoordinates();
+
+        src.reset();
+        assertEquals("size after reset", 0, src.getPointerSize());
+        assertNotSame("xCoordinates after reset", xCoordinates, src.getXCoordinates());
+        assertNotSame("yCoordinates after reset", yCoordinates, src.getYCoordinates());
+        assertNotSame("pointerIds after reset", pointerIds, src.getPointerIds());
+        assertNotSame("times after reset", times, src.getTimes());
+    }
+
+    public void testAdd() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        final int limit = src.getXCoordinates().length * 2 + 10;
+        for (int i = 0; i < limit; i++) {
+            src.addPointer(i, i * 2, i * 3, i * 4);
+            assertEquals("size after add " + i, i + 1, src.getPointerSize());
+        }
+        for (int i = 0; i < limit; i++) {
+            assertEquals("xCoordinates at " + i, i, src.getXCoordinates()[i]);
+            assertEquals("yCoordinates at " + i, i * 2, src.getYCoordinates()[i]);
+            assertEquals("pointerIds at " + i, i * 3, src.getPointerIds()[i]);
+            assertEquals("times at " + i, i * 4, src.getTimes()[i]);
+        }
+    }
+
+    public void testAddAt() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        final int limit = 1000, step = 100;
+        for (int i = 0; i < limit; i += step) {
+            src.addPointer(i, i, i * 2, i * 3, i * 4);
+            assertEquals("size after add at " + i, i + 1, src.getPointerSize());
+        }
+        for (int i = 0; i < limit; i += step) {
+            assertEquals("xCoordinates at " + i, i, src.getXCoordinates()[i]);
+            assertEquals("yCoordinates at " + i, i * 2, src.getYCoordinates()[i]);
+            assertEquals("pointerIds at " + i, i * 3, src.getPointerIds()[i]);
+            assertEquals("times at " + i, i * 4, src.getTimes()[i]);
+        }
+    }
+
+    public void testSet() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        final int limit = src.getXCoordinates().length * 2 + 10;
+        for (int i = 0; i < limit; i++) {
+            src.addPointer(i, i * 2, i * 3, i * 4);
+        }
+        final InputPointers dst = new InputPointers(DEFAULT_CAPACITY);
+        dst.set(src);
+        assertEquals("size after set", dst.getPointerSize(), src.getPointerSize());
+        assertSame("xCoordinates after set", dst.getXCoordinates(), src.getXCoordinates());
+        assertSame("yCoordinates after set", dst.getYCoordinates(), src.getYCoordinates());
+        assertSame("pointerIds after set", dst.getPointerIds(), src.getPointerIds());
+        assertSame("times after set", dst.getTimes(), src.getTimes());
+    }
+
+    public void testCopy() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        final int limit = 100;
+        for (int i = 0; i < limit; i++) {
+            src.addPointer(i, i * 2, i * 3, i * 4);
+        }
+        final InputPointers dst = new InputPointers(DEFAULT_CAPACITY);
+        dst.copy(src);
+        assertEquals("size after copy", dst.getPointerSize(), src.getPointerSize());
+        assertNotSame("xCoordinates after copy", dst.getXCoordinates(), src.getXCoordinates());
+        assertNotSame("yCoordinates after copy", dst.getYCoordinates(), src.getYCoordinates());
+        assertNotSame("pointerIds after copy", dst.getPointerIds(), src.getPointerIds());
+        assertNotSame("times after copy", dst.getTimes(), src.getTimes());
+        final int size = dst.getPointerSize();
+        assertArrayEquals("xCoordinates values after copy",
+                dst.getXCoordinates(), 0, src.getXCoordinates(), 0, size);
+        assertArrayEquals("yCoordinates values after copy",
+                dst.getYCoordinates(), 0, src.getYCoordinates(), 0, size);
+        assertArrayEquals("pointerIds values after copy",
+                dst.getPointerIds(), 0, src.getPointerIds(), 0, size);
+        assertArrayEquals("times values after copy",
+                dst.getTimes(), 0, src.getTimes(), 0, size);
+    }
+
+    public void testAppend() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        final int srcLen = 100;
+        for (int i = 0; i < srcLen; i++) {
+            src.addPointer(i, i * 2, i * 3, 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(src, 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(src, 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",
+                src.getXCoordinates(), 0, dst.getXCoordinates(), dstLen, srcLen);
+        assertArrayEquals("appended yCoordinates values after append",
+                src.getYCoordinates(), 0, dst.getYCoordinates(), dstLen, srcLen);
+        assertArrayEquals("appended pointerIds values after append",
+                src.getPointerIds(), 0, dst.getPointerIds(), dstLen, srcLen);
+        assertArrayEquals("appended times values after append",
+                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) {
+            return;
+        }
+        if (expecteds == null || actuals == null) {
+            fail(message + ": expecteds=" + expecteds + " actuals=" + actuals);
+        }
+        for (int i = 0; i < length; i++) {
+            assertEquals(message + ": element at " + i,
+                    expecteds[i + expectedPos], actuals[i + actualPos]);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index eb47fd5..fe58cb8 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -39,9 +39,9 @@
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardActionListener;
 
 import java.util.HashMap;
+import java.util.Locale;
 
 public class InputTestsBase extends ServiceTestCase<LatinIME> {
 
@@ -52,7 +52,7 @@
 
     protected LatinIME mLatinIME;
     protected Keyboard mKeyboard;
-    protected TextView mTextView;
+    protected MyTextView mTextView;
     protected InputConnection mInputConnection;
     private final HashMap<String, InputMethodSubtype> mSubtypeMap =
             new HashMap<String, InputMethodSubtype>();
@@ -87,6 +87,27 @@
             return (mSpan instanceof SuggestionSpan) &&
                     0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & ((SuggestionSpan)mSpan).getFlags());
         }
+        public String[] getSuggestions() {
+            return ((SuggestionSpan)mSpan).getSuggestions();
+        }
+    }
+
+    // A helper class to increase control over the TextView
+    public static class MyTextView extends TextView {
+        public Locale mCurrentLocale;
+        public MyTextView(final Context c) {
+            super(c);
+        }
+        public void onAttachedToWindow() {
+            super.onAttachedToWindow();
+        }
+        public Locale getTextServicesLocale() {
+            // This method is necessary because TextView is asking this method for the language
+            // to check the spell in. If we don't override this, the spell checker will run in
+            // whatever language the keyboard is currently set on the test device, ignoring any
+            // settings we do inside the tests.
+            return mCurrentLocale;
+        }
     }
 
     public InputTestsBase() {
@@ -113,7 +134,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mTextView = new TextView(getContext());
+        mTextView = new MyTextView(getContext());
         mTextView.setInputType(InputType.TYPE_CLASS_TEXT);
         mTextView.setEnabled(true);
         setupService();
@@ -130,13 +151,12 @@
                 (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         final ViewGroup vg = new FrameLayout(getContext());
         final View inputView = inflater.inflate(R.layout.input_view, vg);
+        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
         mLatinIME.setInputView(inputView);
         mLatinIME.onBindInput();
         mLatinIME.onCreateInputView();
         mLatinIME.onStartInputView(ei, false);
-        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
         mInputConnection = ic;
-        mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
         changeLanguage("en_US");
     }
 
@@ -222,9 +242,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);
     }
 
@@ -252,17 +270,18 @@
 
     protected void changeLanguage(final String locale) {
         final InputMethodSubtype subtype = mSubtypeMap.get(locale);
+        mTextView.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale);
         if (subtype == null) {
             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/PunctuationTests.java b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
index e1d4c46..0eb3ba4 100644
--- a/tests/src/com/android/inputmethod/latin/PunctuationTests.java
+++ b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
@@ -27,7 +27,7 @@
         final String PUNCTUATION_FROM_STRIP = "!";
         final String EXPECTED_RESULT = "this!! ";
         final boolean defaultNextWordPredictionOption =
-                mLatinIME.getResources().getBoolean(R.bool.config_default_next_word_suggestions);
+                mLatinIME.getResources().getBoolean(R.bool.config_default_next_word_prediction);
         final boolean previousNextWordPredictionOption =
                 setBooleanPreference(NEXT_WORD_PREDICTION_OPTION, false,
                         defaultNextWordPredictionOption);
diff --git a/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java b/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
new file mode 100644
index 0000000..995fc14
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
@@ -0,0 +1,333 @@
+/*
+ * 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;
+
+public class ResizableIntArrayTests extends AndroidTestCase {
+    private static final int DEFAULT_CAPACITY = 48;
+
+    public void testNewInstance() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int[] array = src.getPrimitiveArray();
+        assertEquals("new instance length", 0, src.getLength());
+        assertNotNull("new instance array", array);
+        assertEquals("new instance array length", DEFAULT_CAPACITY, array.length);
+    }
+
+    public void testAdd() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int[] array = src.getPrimitiveArray();
+        int[] array2 = null, array3 = null;
+        final int limit = DEFAULT_CAPACITY * 2 + 10;
+        for (int i = 0; i < limit; i++) {
+            src.add(i);
+            assertEquals("length after add " + i, i + 1, src.getLength());
+            if (i == DEFAULT_CAPACITY) {
+                array2 = src.getPrimitiveArray();
+            }
+            if (i == DEFAULT_CAPACITY * 2) {
+                array3 = src.getPrimitiveArray();
+            }
+            if (i < DEFAULT_CAPACITY) {
+                assertSame("array after add " + i, array, src.getPrimitiveArray());
+            } else if (i < DEFAULT_CAPACITY * 2) {
+                assertSame("array after add " + i, array2, src.getPrimitiveArray());
+            } else if (i < DEFAULT_CAPACITY * 3) {
+                assertSame("array after add " + i, array3, src.getPrimitiveArray());
+            }
+        }
+        for (int i = 0; i < limit; i++) {
+            assertEquals("value at " + i, i, src.get(i));
+        }
+    }
+
+    public void testAddAt() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int limit = DEFAULT_CAPACITY * 10, step = DEFAULT_CAPACITY * 2;
+        for (int i = 0; i < limit; i += step) {
+            src.add(i, i);
+            assertEquals("length after add at " + i, i + 1, src.getLength());
+        }
+        for (int i = 0; i < limit; i += step) {
+            assertEquals("value at " + i, i, src.get(i));
+        }
+    }
+
+    public void testGet() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        try {
+            final int value = src.get(0);
+            fail("get(0) shouldn't succeed");
+        } catch (ArrayIndexOutOfBoundsException e) {
+            // success
+        }
+        try {
+            final int value = src.get(DEFAULT_CAPACITY);
+            fail("get(DEFAULT_CAPACITY) shouldn't succeed");
+        } catch (ArrayIndexOutOfBoundsException e) {
+            // success
+        }
+
+        final int index = DEFAULT_CAPACITY / 2;
+        src.add(index, 100);
+        assertEquals("legth after add at " + index, index + 1, src.getLength());
+        assertEquals("value after add at " + index, 100, src.get(index));
+        assertEquals("value after add at 0", 0, src.get(0));
+        try {
+            final int value = src.get(src.getLength());
+            fail("get(length) shouldn't succeed");
+        } catch (ArrayIndexOutOfBoundsException e) {
+            // success
+        }
+    }
+
+    public void testReset() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int[] array = src.getPrimitiveArray();
+        for (int i = 0; i < DEFAULT_CAPACITY; i++) {
+            src.add(i);
+            assertEquals("length after add " + i, i + 1, src.getLength());
+        }
+
+        final int smallerLength = DEFAULT_CAPACITY / 2;
+        src.reset(smallerLength);
+        final int[] array2 = src.getPrimitiveArray();
+        assertEquals("length after reset", 0, src.getLength());
+        assertNotSame("array after reset", array, array2);
+
+        int[] array3 = null;
+        for (int i = 0; i < DEFAULT_CAPACITY; i++) {
+            src.add(i);
+            assertEquals("length after add " + i, i + 1, src.getLength());
+            if (i == smallerLength) {
+                array3 = src.getPrimitiveArray();
+            }
+            if (i < smallerLength) {
+                assertSame("array after add " + i, array2, src.getPrimitiveArray());
+            } else if (i < smallerLength * 2) {
+                assertSame("array after add " + i, array3, src.getPrimitiveArray());
+            }
+        }
+    }
+
+    public void testSetLength() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int[] array = src.getPrimitiveArray();
+        for (int i = 0; i < DEFAULT_CAPACITY; i++) {
+            src.add(i);
+            assertEquals("length after add " + i, i + 1, src.getLength());
+        }
+
+        final int largerLength = DEFAULT_CAPACITY * 2;
+        src.setLength(largerLength);
+        final int[] array2 = src.getPrimitiveArray();
+        assertEquals("length after larger setLength", largerLength, src.getLength());
+        assertNotSame("array after larger setLength", array, array2);
+        assertEquals("array length after larger setLength", largerLength, array2.length);
+        for (int i = 0; i < largerLength; i++) {
+            final int v = src.get(i);
+            if (i < DEFAULT_CAPACITY) {
+                assertEquals("value at " + i, i, v);
+            } else {
+                assertEquals("value at " + i, 0, v);
+            }
+        }
+
+        final int smallerLength = DEFAULT_CAPACITY / 2;
+        src.setLength(smallerLength);
+        final int[] array3 = src.getPrimitiveArray();
+        assertEquals("length after smaller setLength", smallerLength, src.getLength());
+        assertSame("array after smaller setLength", array2, array3);
+        assertEquals("array length after smaller setLength", largerLength, array3.length);
+        for (int i = 0; i < smallerLength; i++) {
+            assertEquals("value at " + i, i, src.get(i));
+        }
+    }
+
+    public void testSet() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int limit = DEFAULT_CAPACITY * 2 + 10;
+        for (int i = 0; i < limit; i++) {
+            src.add(i);
+        }
+
+        final ResizableIntArray dst = new ResizableIntArray(DEFAULT_CAPACITY);
+        dst.set(src);
+        assertEquals("length after set", dst.getLength(), src.getLength());
+        assertSame("array after set", dst.getPrimitiveArray(), src.getPrimitiveArray());
+    }
+
+    public void testCopy() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        for (int i = 0; i < DEFAULT_CAPACITY; i++) {
+            src.add(i);
+        }
+
+        final ResizableIntArray dst = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int[] array = dst.getPrimitiveArray();
+        dst.copy(src);
+        assertEquals("length after copy", dst.getLength(), src.getLength());
+        assertSame("array after copy", array, dst.getPrimitiveArray());
+        assertNotSame("array after copy", dst.getPrimitiveArray(), src.getPrimitiveArray());
+        assertArrayEquals("values after copy",
+                dst.getPrimitiveArray(), 0, src.getPrimitiveArray(), 0, dst.getLength());
+
+        final int smallerLength = DEFAULT_CAPACITY / 2;
+        dst.reset(smallerLength);
+        final int[] array2 = dst.getPrimitiveArray();
+        dst.copy(src);
+        final int[] array3 = dst.getPrimitiveArray();
+        assertEquals("length after copy to smaller", dst.getLength(), src.getLength());
+        assertNotSame("array after copy to smaller", array2, array3);
+        assertNotSame("array after copy to smaller", array3, src.getPrimitiveArray());
+        assertArrayEquals("values after copy to smaller",
+                dst.getPrimitiveArray(), 0, src.getPrimitiveArray(), 0, dst.getLength());
+    }
+
+    public void testAppend() {
+        final int srcLen = DEFAULT_CAPACITY;
+        final ResizableIntArray src = new ResizableIntArray(srcLen);
+        for (int i = 0; i < srcLen; i++) {
+            src.add(i);
+        }
+        final ResizableIntArray dst = new ResizableIntArray(DEFAULT_CAPACITY * 2);
+        final int[] array = dst.getPrimitiveArray();
+        final int dstLen = DEFAULT_CAPACITY / 2;
+        for (int i = 0; i < dstLen; i++) {
+            final int value = -i - 1;
+            dst.add(value);
+        }
+        final ResizableIntArray dstCopy = new ResizableIntArray(dst.getLength());
+        dstCopy.copy(dst);
+
+        dst.append(src, 0, 0);
+        assertEquals("length after append zero", dstLen, dst.getLength());
+        assertSame("array after append zero", array, dst.getPrimitiveArray());
+        assertArrayEquals("values after append zero",
+                dstCopy.getPrimitiveArray(), 0, dst.getPrimitiveArray(), 0, dstLen);
+
+        dst.append(src, 0, srcLen);
+        assertEquals("length after append", dstLen + srcLen, dst.getLength());
+        assertSame("array after append", array, dst.getPrimitiveArray());
+        assertTrue("primitive length after append",
+                dst.getPrimitiveArray().length >= dstLen + srcLen);
+        assertArrayEquals("original values after append",
+                dstCopy.getPrimitiveArray(), 0, dst.getPrimitiveArray(), 0, dstLen);
+        assertArrayEquals("appended values after append",
+                src.getPrimitiveArray(), 0, dst.getPrimitiveArray(), dstLen, srcLen);
+
+        dst.append(src, 0, srcLen);
+        assertEquals("length after 2nd append", dstLen + srcLen * 2, dst.getLength());
+        assertNotSame("array after 2nd append", array, dst.getPrimitiveArray());
+        assertTrue("primitive length after 2nd append",
+                dst.getPrimitiveArray().length >= dstLen + srcLen * 2);
+        assertArrayEquals("original values after 2nd append",
+                dstCopy.getPrimitiveArray(), 0, dst.getPrimitiveArray(), 0, dstLen);
+        assertArrayEquals("appended values after 2nd append",
+                src.getPrimitiveArray(), 0, dst.getPrimitiveArray(), dstLen, srcLen);
+        assertArrayEquals("appended values after 2nd append",
+                src.getPrimitiveArray(), 0, dst.getPrimitiveArray(), dstLen + srcLen, srcLen);
+    }
+
+    public void testFill() {
+        final int srcLen = DEFAULT_CAPACITY;
+        final ResizableIntArray src = new ResizableIntArray(srcLen);
+        for (int i = 0; i < srcLen; i++) {
+            src.add(i);
+        }
+        final int[] array = src.getPrimitiveArray();
+
+        final int startPos = srcLen / 3;
+        final int length = srcLen / 3;
+        final int endPos = startPos + length;
+        assertTrue(startPos >= 1);
+        final int value = 123;
+        try {
+            src.fill(value, -1, length);
+            fail("fill from -1 shouldn't succeed");
+        } catch (IllegalArgumentException e) {
+            // success
+        }
+        try {
+            src.fill(value, startPos, -1);
+            fail("fill negative length shouldn't succeed");
+        } catch (IllegalArgumentException e) {
+            // success
+        }
+
+        src.fill(value, startPos, length);
+        assertEquals("length after fill", srcLen, src.getLength());
+        assertSame("array after fill", array, src.getPrimitiveArray());
+        for (int i = 0; i < srcLen; i++) {
+            final int v = src.get(i);
+            if (i >= startPos && i < endPos) {
+                assertEquals("new values after fill at " + i, value, v);
+            } else {
+                assertEquals("unmodified values after fill at " + i, i, v);
+            }
+        }
+
+        final int length2 = srcLen * 2 - startPos;
+        final int largeEnd = startPos + length2;
+        assertTrue(largeEnd > srcLen);
+        final int value2 = 456;
+        src.fill(value2, startPos, length2);
+        assertEquals("length after large fill", largeEnd, src.getLength());
+        assertNotSame("array after large fill", array, src.getPrimitiveArray());
+        for (int i = 0; i < largeEnd; i++) {
+            final int v = src.get(i);
+            if (i >= startPos && i < largeEnd) {
+                assertEquals("new values after large fill at " + i, value2, v);
+            } else {
+                assertEquals("unmodified values after large fill at " + i, i, v);
+            }
+        }
+
+        final int startPos2 = largeEnd + length2;
+        final int endPos2 = startPos2 + length2;
+        final int value3 = 789;
+        src.fill(value3, startPos2, length2);
+        assertEquals("length after disjoint fill", endPos2, src.getLength());
+        for (int i = 0; i < endPos2; i++) {
+            final int v = src.get(i);
+            if (i >= startPos2 && i < endPos2) {
+                assertEquals("new values after disjoint fill at " + i, value3, v);
+            } else if (i >= startPos && i < largeEnd) {
+                assertEquals("unmodified values after disjoint fill at " + i, value2, v);
+            } else if (i < startPos) {
+                assertEquals("unmodified values after disjoint fill at " + i, i, v);
+            } else {
+                assertEquals("gap values after disjoint fill at " + i, 0, v);
+            }
+        }
+    }
+
+    private static void assertArrayEquals(String message, int[] expecteds, int expectedPos,
+            int[] actuals, int actualPos, int length) {
+        if (expecteds == null && actuals == null) {
+            return;
+        }
+        if (expecteds == null || actuals == null) {
+            fail(message + ": expecteds=" + expecteds + " actuals=" + actuals);
+        }
+        for (int i = 0; i < length; i++) {
+            assertEquals(message + ": element at " + i,
+                    expecteds[i + expectedPos], actuals[i + actualPos]);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
new file mode 100644
index 0000000..ad99379
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.inputmethodservice.InputMethodService;
+import android.test.AndroidTestCase;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+
+import com.android.inputmethod.latin.RichInputConnection.Range;
+
+public class RichInputConnectionTests extends AndroidTestCase {
+
+    // The following is meant to be a reasonable default for
+    // the "word_separators" resource.
+    private static final String sSeparators = ".,:;!?-";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    private class MockConnection extends InputConnectionWrapper {
+        final String mTextBefore;
+        final String mTextAfter;
+        final ExtractedText mExtractedText;
+
+        public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) {
+            super(null, false);
+            mTextBefore = textBefore;
+            mTextAfter = textAfter;
+            mExtractedText = extractedText;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int)
+         */
+        @Override
+        public CharSequence getTextBeforeCursor(int n, int flags) {
+            return mTextBefore;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int)
+         */
+        @Override
+        public CharSequence getTextAfterCursor(int n, int flags) {
+            return mTextAfter;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText(
+         *         ExtractedTextRequest, int)
+         */
+        @Override
+        public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+            return mExtractedText;
+        }
+
+        @Override
+        public boolean beginBatchEdit() {
+            return true;
+        }
+
+        @Override
+        public boolean endBatchEdit() {
+            return true;
+        }
+    }
+
+    private class MockInputMethodService extends InputMethodService {
+        InputConnection mInputConnection;
+        public void setInputConnection(final InputConnection inputConnection) {
+            mInputConnection = inputConnection;
+        }
+        @Override
+        public InputConnection getCurrentInputConnection() {
+            return mInputConnection;
+        }
+    }
+
+    /************************** Tests ************************/
+
+    /**
+     * Test for getting previous word (for bigram suggestions)
+     */
+    public void testGetPreviousWord() {
+        // If one of the following cases breaks, the bigram suggestions won't work.
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 2), "abc");
+        assertNull(RichInputConnection.getNthPreviousWord("abc", sSeparators, 2));
+        assertNull(RichInputConnection.getNthPreviousWord("abc. def", sSeparators, 2));
+
+        // The following tests reflect the current behavior of the function
+        // RichInputConnection#getNthPreviousWord.
+        // TODO: However at this time, the code does never go
+        // into such a path, so it should be safe to change the behavior of
+        // this function if needed - especially since it does not seem very
+        // logical. These tests are just there to catch any unintentional
+        // changes in the behavior of the RichInputConnection#getPreviousWord method.
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 2), "abc");
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 2), "abc");
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 2), "def");
+        assertNull(RichInputConnection.getNthPreviousWord("abc ", sSeparators, 2));
+
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 1), "def");
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 1), "def");
+        assertNull(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 1));
+        assertNull(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 1));
+    }
+
+    /**
+     * Test logic in getting the word range at the cursor.
+     */
+    public void testGetWordRangeAtCursor() {
+        ExtractedText et = new ExtractedText();
+        final MockInputMethodService mockInputMethodService = new MockInputMethodService();
+        final RichInputConnection ic = new RichInputConnection(mockInputMethodService);
+        mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et));
+        et.startOffset = 0;
+        et.selectionStart = 7;
+        Range r;
+
+        ic.beginBatchEdit();
+        // basic case
+        r = ic.getWordRangeAtCursor(" ", 0);
+        assertEquals("word", r.mWord);
+
+        // more than one word
+        r = ic.getWordRangeAtCursor(" ", 1);
+        assertEquals("word word", r.mWord);
+        ic.endBatchEdit();
+
+        // tab character instead of space
+        mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
+        ic.beginBatchEdit();
+        r = ic.getWordRangeAtCursor("\t", 1);
+        ic.endBatchEdit();
+        assertEquals("word\tword", r.mWord);
+
+        // only one word doesn't go too far
+        mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
+        ic.beginBatchEdit();
+        r = ic.getWordRangeAtCursor("\t", 1);
+        ic.endBatchEdit();
+        assertEquals("word\tword", r.mWord);
+
+        // tab or space
+        mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
+        ic.beginBatchEdit();
+        r = ic.getWordRangeAtCursor(" \t", 1);
+        ic.endBatchEdit();
+        assertEquals("word\tword", r.mWord);
+
+        // tab or space multiword
+        mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
+        ic.beginBatchEdit();
+        r = ic.getWordRangeAtCursor(" \t", 2);
+        ic.endBatchEdit();
+        assertEquals("one word\tword", r.mWord);
+
+        // splitting on supplementary character
+        final String supplementaryChar = "\uD840\uDC8A";
+        mockInputMethodService.setInputConnection(
+                new MockConnection("one word" + supplementaryChar + "wo", "rd", et));
+        ic.beginBatchEdit();
+        r = ic.getWordRangeAtCursor(supplementaryChar, 0);
+        ic.endBatchEdit();
+        assertEquals("word", r.mWord);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
index 5db06ef..be3494d 100644
--- a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
@@ -17,6 +17,9 @@
 package com.android.inputmethod.latin;
 
 import android.test.AndroidTestCase;
+import android.text.TextUtils;
+
+import java.util.Locale;
 
 public class StringUtilsTests extends AndroidTestCase {
     public void testContainsInArray() {
@@ -89,14 +92,57 @@
                 StringUtils.removeFromCsvIfExists("key", "key1,key,key3,key,key5"));
     }
 
-    public void testHasUpperCase() {
-        assertTrue("single upper-case string", StringUtils.hasUpperCase("String"));
-        assertTrue("multi upper-case string", StringUtils.hasUpperCase("stRInG"));
-        assertTrue("all upper-case string", StringUtils.hasUpperCase("STRING"));
-        assertTrue("upper-case string with non-letters", StringUtils.hasUpperCase("He's"));
+    private void onePathForCaps(final CharSequence cs, final int expectedResult, final int mask,
+            final Locale l, final boolean hasSpaceBefore) {
+        int oneTimeResult = expectedResult & mask;
+        assertEquals("After >" + cs + "<", oneTimeResult,
+                StringUtils.getCapsMode(cs, mask, l, hasSpaceBefore));
+    }
 
-        assertFalse("empty string", StringUtils.hasUpperCase(""));
-        assertFalse("lower-case string", StringUtils.hasUpperCase("string"));
-        assertFalse("lower-case string with non-letters", StringUtils.hasUpperCase("he's"));
+    private void allPathsForCaps(final CharSequence cs, final int expectedResult, final Locale l,
+            final boolean hasSpaceBefore) {
+        final int c = TextUtils.CAP_MODE_CHARACTERS;
+        final int w = TextUtils.CAP_MODE_WORDS;
+        final int s = TextUtils.CAP_MODE_SENTENCES;
+        onePathForCaps(cs, expectedResult, c | w | s, l, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, w | s, l, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, c | s, l, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, c | w, l, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, c, l, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, w, l, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, s, l, hasSpaceBefore);
+    }
+
+    public void testGetCapsMode() {
+        final int c = TextUtils.CAP_MODE_CHARACTERS;
+        final int w = TextUtils.CAP_MODE_WORDS;
+        final int s = TextUtils.CAP_MODE_SENTENCES;
+        Locale l = Locale.ENGLISH;
+        allPathsForCaps("", c | w | s, l, false);
+        allPathsForCaps("Word", c, l, false);
+        allPathsForCaps("Word.", c, l, false);
+        allPathsForCaps("Word ", c | w, l, false);
+        allPathsForCaps("Word. ", c | w | s, l, false);
+        allPathsForCaps("Word..", c, l, false);
+        allPathsForCaps("Word.. ", c | w | s, l, false);
+        allPathsForCaps("Word... ", c | w | s, l, false);
+        allPathsForCaps("Word ... ", c | w | s, l, false);
+        allPathsForCaps("Word . ", c | w, l, false);
+        allPathsForCaps("In the U.S ", c | w, l, false);
+        allPathsForCaps("In the U.S. ", c | w, l, false);
+        allPathsForCaps("Some stuff (e.g. ", c | w, l, false);
+        allPathsForCaps("In the U.S.. ", c | w | s, l, false);
+        allPathsForCaps("\"Word.\" ", c | w | s, l, false);
+        allPathsForCaps("\"Word\". ", c | w | s, l, false);
+        allPathsForCaps("\"Word\" ", c | w, l, false);
+
+        // Test for phantom space
+        allPathsForCaps("Word", c | w, l, true);
+        allPathsForCaps("Word.", c | w | s, l, true);
+
+        l = Locale.FRENCH;
+        allPathsForCaps("\"Word.\" ", c | w, l, false);
+        allPathsForCaps("\"Word\". ", c | w | s, l, false);
+        allPathsForCaps("\"Word\" ", c | w, l, false);
     }
 }
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/tests/src/com/android/inputmethod/latin/UserHistoryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/UserHistoryDictIOUtilsTests.java
new file mode 100644
index 0000000..70f916c
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/UserHistoryDictIOUtilsTests.java
@@ -0,0 +1,252 @@
+/*
+ * 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.UserHistoryDictIOUtils.BigramDictionaryInterface;
+import com.android.inputmethod.latin.UserHistoryDictIOUtils.OnAddWordListener;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FormatSpec;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * Unit tests for UserHistoryDictIOUtils
+ */
+public class UserHistoryDictIOUtilsTests extends AndroidTestCase
+    implements BigramDictionaryInterface {
+
+    private static final String TAG = UserHistoryDictIOUtilsTests.class.getSimpleName();
+    private static final int UNIGRAM_FREQUENCY = 50;
+    private static final int BIGRAM_FREQUENCY = 100;
+    private static final ArrayList<String> NOT_HAVE_BIGRAM = new ArrayList<String>();
+    private static final FormatSpec.FormatOptions FORMAT_OPTIONS = new FormatSpec.FormatOptions(2);
+
+    /**
+     * Return same frequency for all words and bigrams
+     */
+    @Override
+    public int getFrequency(String word1, String word2) {
+        if (word1 == null) return UNIGRAM_FREQUENCY;
+        return BIGRAM_FREQUENCY;
+    }
+
+    // Utilities for Testing
+
+    private void addWord(final String word,
+            final HashMap<String, ArrayList<String> > addedWords) {
+        if (!addedWords.containsKey(word)) {
+            addedWords.put(word, new ArrayList<String>());
+        }
+    }
+
+    private void addBigram(final String word1, final String word2,
+            final HashMap<String, ArrayList<String> > addedWords) {
+        addWord(word1, addedWords);
+        addWord(word2, addedWords);
+        addedWords.get(word1).add(word2);
+    }
+
+    private void addBigramToBigramList(final String word1, final String word2,
+            final HashMap<String, ArrayList<String> > addedWords,
+            final UserHistoryDictionaryBigramList bigramList) {
+        bigramList.addBigram(null, word1);
+        bigramList.addBigram(word1, word2);
+
+        addBigram(word1, word2, addedWords);
+    }
+
+    private void checkWordInFusionDict(final FusionDictionary dict, final String word,
+            final ArrayList<String> expectedBigrams) {
+        final CharGroup group = FusionDictionary.findWordInTree(dict.mRoot, word);
+        assertNotNull(group);
+        assertTrue(group.isTerminal());
+
+        for (final String bigram : expectedBigrams) {
+            assertNotNull(group.getBigram(bigram));
+        }
+    }
+
+    private void checkWordsInFusionDict(final FusionDictionary dict,
+            final HashMap<String, ArrayList<String> > bigrams) {
+        for (final String word : bigrams.keySet()) {
+            if (bigrams.containsKey(word)) {
+                checkWordInFusionDict(dict, word, bigrams.get(word));
+            } else {
+                checkWordInFusionDict(dict, word, NOT_HAVE_BIGRAM);
+            }
+        }
+    }
+
+    private void checkWordInBigramList(
+            final UserHistoryDictionaryBigramList bigramList, final String word,
+            final ArrayList<String> expectedBigrams) {
+        // check unigram
+        final HashMap<String,Byte> unigramMap = bigramList.getBigrams(null);
+        assertTrue(unigramMap.containsKey(word));
+
+        // check bigrams
+        final ArrayList<String> actualBigrams = new ArrayList<String>(
+                bigramList.getBigrams(word).keySet());
+
+        Collections.sort(expectedBigrams);
+        Collections.sort(actualBigrams);
+        assertEquals(expectedBigrams, actualBigrams);
+    }
+
+    private void checkWordsInBigramList(final UserHistoryDictionaryBigramList bigramList,
+            final HashMap<String, ArrayList<String> > addedWords) {
+        for (final String word : addedWords.keySet()) {
+            if (addedWords.containsKey(word)) {
+                checkWordInBigramList(bigramList, word, addedWords.get(word));
+            } else {
+                checkWordInBigramList(bigramList, word, NOT_HAVE_BIGRAM);
+            }
+        }
+    }
+
+    private void writeDictToFile(final File file,
+            final UserHistoryDictionaryBigramList bigramList) {
+        try {
+            final FileOutputStream out = new FileOutputStream(file);
+            UserHistoryDictIOUtils.writeDictionaryBinary(out, this, bigramList, FORMAT_OPTIONS);
+            out.flush();
+            out.close();
+        } catch (IOException e) {
+            Log.e(TAG, "IO exception while writing file: " + e);
+        }
+    }
+
+    private void readDictFromFile(final File file, final OnAddWordListener listener) {
+        FileInputStream inStream = null;
+
+        try {
+            inStream = new FileInputStream(file);
+            final byte[] buffer = new byte[(int)file.length()];
+            inStream.read(buffer);
+
+            UserHistoryDictIOUtils.readDictionaryBinary(
+                    new UserHistoryDictIOUtils.ByteArrayWrapper(buffer), listener);
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, "file not found: " + e);
+        } catch (IOException e) {
+            Log.e(TAG, "IOException: " + e);
+        } finally {
+            if (inStream != null) {
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    // do nothing
+                }
+            }
+        }
+    }
+
+    public void testGenerateFusionDictionary() {
+        final UserHistoryDictionaryBigramList originalList = new UserHistoryDictionaryBigramList();
+
+        final HashMap<String, ArrayList<String> > addedWords =
+                new HashMap<String, ArrayList<String>>();
+        addBigramToBigramList("this", "is", addedWords, originalList);
+        addBigramToBigramList("this", "was", addedWords, originalList);
+        addBigramToBigramList("hello", "world", addedWords, originalList);
+
+        final FusionDictionary fusionDict =
+                UserHistoryDictIOUtils.constructFusionDictionary(this, originalList);
+
+        checkWordsInFusionDict(fusionDict, addedWords);
+    }
+
+    public void testReadAndWrite() {
+        final Context context = getContext();
+
+        File file = null;
+        try {
+            file = File.createTempFile("testReadAndWrite", ".dict");
+        } catch (IOException e) {
+            Log.d(TAG, "IOException while creating a temporary file: " + e);
+        }
+        assertNotNull(file);
+
+        // make original dictionary
+        final UserHistoryDictionaryBigramList originalList = new UserHistoryDictionaryBigramList();
+        final HashMap<String, ArrayList<String>> addedWords = CollectionUtils.newHashMap();
+        addBigramToBigramList("this" , "is"   , addedWords, originalList);
+        addBigramToBigramList("this" , "was"  , addedWords, originalList);
+        addBigramToBigramList("is"   , "not"  , addedWords, originalList);
+        addBigramToBigramList("hello", "world", addedWords, originalList);
+
+        // write to file
+        writeDictToFile(file, originalList);
+
+        // make result dict.
+        final UserHistoryDictionaryBigramList resultList = new UserHistoryDictionaryBigramList();
+        final OnAddWordListener listener = new OnAddWordListener() {
+            @Override
+            public void setUnigram(final String word,
+                    final String shortcutTarget, final int frequency) {
+                Log.d(TAG, "in: setUnigram: " + word + "," + frequency);
+                resultList.addBigram(null, word, (byte)frequency);
+            }
+            @Override
+            public void setBigram(final String word1, final String word2, final int frequency) {
+                Log.d(TAG, "in: setBigram: " + word1 + "," + word2 + "," + frequency);
+                resultList.addBigram(word1, word2, (byte)frequency);
+            }
+        };
+
+        // load from file
+        readDictFromFile(file, listener);
+        checkWordsInBigramList(resultList, addedWords);
+
+        // add new bigram
+        addBigramToBigramList("hello", "java", addedWords, resultList);
+
+        // rewrite
+        writeDictToFile(file, resultList);
+        final UserHistoryDictionaryBigramList resultList2 = new UserHistoryDictionaryBigramList();
+        final OnAddWordListener listener2 = new OnAddWordListener() {
+            @Override
+            public void setUnigram(final String word,
+                    final String shortcutTarget, final int frequency) {
+                Log.d(TAG, "in: setUnigram: " + word + "," + frequency);
+                resultList2.addBigram(null, word, (byte)frequency);
+            }
+            @Override
+            public void setBigram(final String word1, final String word2, final int frequency) {
+                Log.d(TAG, "in: setBigram: " + word1 + "," + word2 + "," + frequency);
+                resultList2.addBigram(word1, word2, (byte)frequency);
+            }
+        };
+
+        // load from file
+        readDictFromFile(file, listener2);
+        checkWordsInBigramList(resultList2, addedWords);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/UtilsTests.java b/tests/src/com/android/inputmethod/latin/UtilsTests.java
deleted file mode 100644
index 2ef4e2f..0000000
--- a/tests/src/com/android/inputmethod/latin/UtilsTests.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2010,2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.test.AndroidTestCase;
-
-public class UtilsTests extends AndroidTestCase {
-
-    // The following is meant to be a reasonable default for
-    // the "word_separators" resource.
-    private static final String sSeparators = ".,:;!?-";
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-    }
-
-    /************************** Tests ************************/
-
-    /**
-     * Test for getting previous word (for bigram suggestions)
-     */
-    public void testGetPreviousWord() {
-        // If one of the following cases breaks, the bigram suggestions won't work.
-        assertEquals(EditingUtils.getPreviousWord("abc def", sSeparators), "abc");
-        assertNull(EditingUtils.getPreviousWord("abc", sSeparators));
-        assertNull(EditingUtils.getPreviousWord("abc. def", sSeparators));
-
-        // The following tests reflect the current behavior of the function
-        // EditingUtils#getPreviousWord.
-        // TODO: However at this time, the code does never go
-        // into such a path, so it should be safe to change the behavior of
-        // this function if needed - especially since it does not seem very
-        // logical. These tests are just there to catch any unintentional
-        // changes in the behavior of the EditingUtils#getPreviousWord method.
-        assertEquals(EditingUtils.getPreviousWord("abc def ", sSeparators), "abc");
-        assertEquals(EditingUtils.getPreviousWord("abc def.", sSeparators), "abc");
-        assertEquals(EditingUtils.getPreviousWord("abc def .", sSeparators), "def");
-        assertNull(EditingUtils.getPreviousWord("abc ", sSeparators));
-    }
-
-    /**
-     * Test for getting the word before the cursor (for bigram)
-     */
-    public void testGetThisWord() {
-        assertEquals(EditingUtils.getThisWord("abc def", sSeparators), "def");
-        assertEquals(EditingUtils.getThisWord("abc def ", sSeparators), "def");
-        assertNull(EditingUtils.getThisWord("abc def.", sSeparators));
-        assertNull(EditingUtils.getThisWord("abc def .", sSeparators));
-    }
-}
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
new file mode 100644
index 0000000..539021f
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
@@ -0,0 +1,602 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.UserHistoryDictIOUtils;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+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 int TOLERANCE_OF_BIGRAM_FREQ = 5;
+
+    private static final int USE_BYTE_ARRAY = 1;
+    private static final int USE_BYTE_BUFFER = 2;
+
+    private static final List<String> sWords = CollectionUtils.newArrayList();
+    private static final SparseArray<List<Integer>> sEmptyBigrams =
+            CollectionUtils.newSparseArray();
+    private static final SparseArray<List<Integer>> sStarBigrams = CollectionUtils.newSparseArray();
+    private static final SparseArray<List<Integer>> sChainBigrams =
+            CollectionUtils.newSparseArray();
+
+    private static final FormatSpec.FormatOptions VERSION2 = new FormatSpec.FormatOptions(2);
+    private static final FormatSpec.FormatOptions VERSION3_WITHOUT_PARENTADDRESS =
+            new FormatSpec.FormatOptions(3, false /* hasParentAddress */);
+    private static final FormatSpec.FormatOptions VERSION3_WITH_PARENTADDRESS =
+            new FormatSpec.FormatOptions(3, true /* hasParentAddress */);
+    private static final FormatSpec.FormatOptions VERSION3_WITH_LINKEDLIST_NODE =
+            new FormatSpec.FormatOptions(3, true /* hasParentAddress */,
+                    true /* hasLinkedListNode */);
+
+    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"
+    };
+
+    public BinaryDictIOTests() {
+        super();
+
+        final Random random = new Random(123456);
+        sWords.clear();
+        generateWords(MAX_UNIGRAMS, random);
+
+        for (int i = 0; i < sWords.size(); ++i) {
+            sChainBigrams.put(i, new ArrayList<Integer>());
+            if (i > 0) {
+                sChainBigrams.get(i - 1).add(i);
+            }
+        }
+
+        sStarBigrams.put(0, new ArrayList<Integer>());
+        for (int i = 1; i < sWords.size(); ++i) {
+            sStarBigrams.get(0).add(i);
+        }
+    }
+
+    // Utilities for test
+
+    /**
+     * Makes new buffer according to BUFFER_TYPE.
+     */
+    private FusionDictionaryBufferInterface getBuffer(final File file, final int bufferType) {
+        FileInputStream inStream = null;
+        try {
+            inStream = new FileInputStream(file);
+            if (bufferType == USE_BYTE_ARRAY) {
+                final byte[] array = new byte[(int)file.length()];
+                inStream.read(array);
+                return new UserHistoryDictIOUtils.ByteArrayWrapper(array);
+            } else if (bufferType == USE_BYTE_BUFFER){
+                final ByteBuffer buffer = inStream.getChannel().map(
+                        FileChannel.MapMode.READ_ONLY, 0, file.length());
+                return new BinaryDictInputOutput.ByteBufferWrapper(buffer);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "IOException while making buffer: " + e);
+        } finally {
+            if (inStream != null) {
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "IOException while closing stream: " + e);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 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 void generateWords(final int number, final Random random) {
+        final Set<String> wordSet = CollectionUtils.newHashSet();
+        while (wordSet.size() < number) {
+            wordSet.add(generateWord(random.nextInt()));
+        }
+        sWords.addAll(wordSet);
+    }
+
+    /**
+     * Adds unigrams to the dictionary.
+     */
+    private void addUnigrams(final int number, final FusionDictionary dict,
+            final List<String> words, final Map<String, List<String>> shortcutMap) {
+        for (int i = 0; i < number; ++i) {
+            final String word = words.get(i);
+            final ArrayList<WeightedString> shortcuts = CollectionUtils.newArrayList();
+            if (shortcutMap != null && shortcutMap.containsKey(word)) {
+                for (final String shortcut : shortcutMap.get(word)) {
+                    shortcuts.add(new WeightedString(shortcut, UNIGRAM_FREQ));
+                }
+            }
+            dict.add(word, UNIGRAM_FREQ, (shortcutMap == null) ? null : shortcuts,
+                    false /* isNotAWord */);
+        }
+    }
+
+    private void addBigrams(final FusionDictionary dict,
+            final List<String> words,
+            final SparseArray<List<Integer>> bigrams) {
+        for (int i = 0; i < bigrams.size(); ++i) {
+            final int w1 = bigrams.keyAt(i);
+            for (int w2 : bigrams.valueAt(i)) {
+                dict.setBigram(words.get(w1), words.get(w2), BIGRAM_FREQ);
+            }
+        }
+    }
+
+    private long timeWritingDictToFile(final File file, final FusionDictionary dict,
+            final FormatSpec.FormatOptions formatOptions) {
+
+        long now = -1, diff = -1;
+
+        try {
+            final FileOutputStream out = new FileOutputStream(file);
+
+            now = System.currentTimeMillis();
+            BinaryDictInputOutput.writeDictionaryBinary(out, dict, formatOptions);
+            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, final Map<String, List<String>> shortcutMap) {
+        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)));
+            }
+        }
+
+        // check shortcut
+        if (shortcutMap != null) {
+            for (final Map.Entry<String, List<String>> entry : shortcutMap.entrySet()) {
+                final CharGroup group = FusionDictionary.findWordInTree(dict.mRoot, entry.getKey());
+                for (final String word : entry.getValue()) {
+                    assertNotNull("shortcut not found: " + entry.getKey() + ", " + word,
+                            group.getShortcut(word));
+                }
+            }
+        }
+    }
+
+    private String outputOptions(final int bufferType,
+            final FormatSpec.FormatOptions formatOptions) {
+        String result = " : buffer type = "
+                + ((bufferType == USE_BYTE_BUFFER) ? "byte buffer" : "byte array");
+        result += " : version = " + formatOptions.mVersion;
+        return result + ", hasParentAddress = " + formatOptions.mHasParentAddress
+                + ", hasLinkedListNode = " + formatOptions.mHasLinkedListNode;
+    }
+
+    // Tests for readDictionaryBinary and writeDictionaryBinary
+
+    private long timeReadingAndCheckDict(final File file, final List<String> words,
+            final SparseArray<List<Integer>> bigrams, final Map<String, List<String>> shortcutMap,
+            final int bufferType) {
+        long now, diff = -1;
+        final FusionDictionaryBufferInterface buffer = getBuffer(file, bufferType);
+        assertNotNull(buffer);
+
+        FusionDictionary dict = null;
+        try {
+            now = System.currentTimeMillis();
+            dict = BinaryDictInputOutput.readDictionaryBinary(buffer, null);
+            diff  = System.currentTimeMillis() - now;
+        } catch (IOException e) {
+            Log.e(TAG, "IOException while reading dictionary: " + e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "Unsupported format: " + e);
+        }
+
+        checkDictionary(dict, words, bigrams, shortcutMap);
+        return diff;
+    }
+
+    // Tests for readDictionaryBinary and writeDictionaryBinary
+    private String runReadAndWrite(final List<String> words,
+            final SparseArray<List<Integer>> bigrams, final Map<String, List<String>> shortcuts,
+            final int bufferType, final FormatSpec.FormatOptions formatOptions,
+            final String message) {
+        File file = null;
+        try {
+            file = File.createTempFile("runReadAndWrite", ".dict");
+        } catch (IOException e) {
+            Log.e(TAG, "IOException: " + e);
+        }
+        assertNotNull(file);
+
+        final FusionDictionary dict = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(new HashMap<String,String>(), false, false));
+        addUnigrams(words.size(), dict, words, shortcuts);
+        addBigrams(dict, words, bigrams);
+        checkDictionary(dict, words, bigrams, shortcuts);
+
+        final long write = timeWritingDictToFile(file, dict, formatOptions);
+        final long read = timeReadingAndCheckDict(file, words, bigrams, shortcuts, bufferType);
+
+        return "PROF: read=" + read + "ms, write=" + write + "ms :" + message
+                + " : " + outputOptions(bufferType, formatOptions);
+    }
+
+    private void runReadAndWriteTests(final List<String> results, final int bufferType,
+            final FormatSpec.FormatOptions formatOptions) {
+        results.add(runReadAndWrite(sWords, sEmptyBigrams, null /* shortcuts */, bufferType,
+                formatOptions, "unigram"));
+        results.add(runReadAndWrite(sWords, sChainBigrams, null /* shortcuts */, bufferType,
+                formatOptions, "chain"));
+        results.add(runReadAndWrite(sWords, sStarBigrams, null /* shortcuts */, bufferType,
+                formatOptions, "star"));
+    }
+
+    public void testReadAndWriteWithByteBuffer() {
+        final List<String> results = CollectionUtils.newArrayList();
+
+        runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION2);
+        runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION3_WITHOUT_PARENTADDRESS);
+        runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION3_WITH_PARENTADDRESS);
+        runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION3_WITH_LINKEDLIST_NODE);
+
+        for (final String result : results) {
+            Log.d(TAG, result);
+        }
+    }
+
+    public void testReadAndWriteWithByteArray() {
+        final List<String> results = CollectionUtils.newArrayList();
+
+        runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION2);
+        runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION3_WITHOUT_PARENTADDRESS);
+        runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION3_WITH_PARENTADDRESS);
+        runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION3_WITH_LINKEDLIST_NODE);
+
+        for (final String result : results) {
+            Log.d(TAG, result);
+        }
+    }
+
+    // Tests for readUnigramsAndBigramsBinary
+
+    private void checkWordMap(final List<String> expectedWords,
+            final SparseArray<List<Integer>> expectedBigrams,
+            final Map<Integer, String> resultWords,
+            final Map<Integer, Integer> resultFrequencies,
+            final Map<Integer, ArrayList<PendingAttribute>> resultBigrams) {
+        // check unigrams
+        final Set<String> actualWordsSet = new HashSet<String>(resultWords.values());
+        final Set<String> expectedWordsSet = new HashSet<String>(expectedWords);
+        assertEquals(actualWordsSet, expectedWordsSet);
+
+        for (int freq : resultFrequencies.values()) {
+            assertEquals(freq, UNIGRAM_FREQ);
+        }
+
+        // check bigrams
+        final Map<String, List<String>> expBigrams = new HashMap<String, List<String>>();
+        for (int i = 0; i < expectedBigrams.size(); ++i) {
+            final String word1 = expectedWords.get(expectedBigrams.keyAt(i));
+            for (int w2 : expectedBigrams.valueAt(i)) {
+                if (expBigrams.get(word1) == null) {
+                    expBigrams.put(word1, new ArrayList<String>());
+                }
+                expBigrams.get(word1).add(expectedWords.get(w2));
+            }
+        }
+
+        final Map<String, List<String>> actBigrams = new HashMap<String, List<String>>();
+        for (Entry<Integer, ArrayList<PendingAttribute>> entry : resultBigrams.entrySet()) {
+            final String word1 = resultWords.get(entry.getKey());
+            final int unigramFreq = resultFrequencies.get(entry.getKey());
+            for (PendingAttribute attr : entry.getValue()) {
+                final String word2 = resultWords.get(attr.mAddress);
+                if (actBigrams.get(word1) == null) {
+                    actBigrams.put(word1, new ArrayList<String>());
+                }
+                actBigrams.get(word1).add(word2);
+
+                final int bigramFreq = BinaryDictInputOutput.reconstructBigramFrequency(
+                        unigramFreq, attr.mFrequency);
+                assertTrue(Math.abs(bigramFreq - BIGRAM_FREQ) < TOLERANCE_OF_BIGRAM_FREQ);
+            }
+        }
+
+        assertEquals(actBigrams, expBigrams);
+    }
+
+    private long timeAndCheckReadUnigramsAndBigramsBinary(final File file, final List<String> words,
+            final SparseArray<List<Integer>> bigrams, final int bufferType) {
+        FileInputStream inStream = null;
+
+        final Map<Integer, String> resultWords = CollectionUtils.newTreeMap();
+        final Map<Integer, ArrayList<PendingAttribute>> resultBigrams =
+                CollectionUtils.newTreeMap();
+        final Map<Integer, Integer> resultFreqs = CollectionUtils.newTreeMap();
+
+        long now = -1, diff = -1;
+        final FusionDictionaryBufferInterface buffer = getBuffer(file, bufferType);
+        assertNotNull("Can't get buffer.", buffer);
+        try {
+            now = System.currentTimeMillis();
+            BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, resultWords, resultFreqs,
+                    resultBigrams);
+            diff = System.currentTimeMillis() - now;
+        } catch (IOException e) {
+            Log.e(TAG, "IOException " + e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "UnsupportedFormatException: " + e);
+        } finally {
+            if (inStream != null) {
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    // do nothing
+                }
+            }
+        }
+
+        checkWordMap(words, bigrams, resultWords, resultFreqs, resultBigrams);
+        return diff;
+    }
+
+    private String runReadUnigramsAndBigramsBinary(final List<String> words,
+            final SparseArray<List<Integer>> bigrams, final int bufferType,
+            final FormatSpec.FormatOptions formatOptions, final String message) {
+        File file = null;
+        try {
+            file = File.createTempFile("runReadUnigrams", ".dict");
+        } catch (IOException e) {
+            Log.e(TAG, "IOException: " + e);
+        }
+        assertNotNull(file);
+
+        // making the dictionary from lists of words.
+        final FusionDictionary dict = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(
+                        new HashMap<String, String>(), false, false));
+        addUnigrams(words.size(), dict, words, null /* shortcutMap */);
+        addBigrams(dict, words, bigrams);
+
+        timeWritingDictToFile(file, dict, formatOptions);
+
+        long wordMap = timeAndCheckReadUnigramsAndBigramsBinary(file, words, bigrams, bufferType);
+        long fullReading = timeReadingAndCheckDict(file, words, bigrams, null /* shortcutMap */,
+                bufferType);
+
+        return "readDictionaryBinary=" + fullReading + ", readUnigramsAndBigramsBinary=" + wordMap
+                + " : " + message + " : " + outputOptions(bufferType, formatOptions);
+    }
+
+    private void runReadUnigramsAndBigramsTests(final List<String> results, final int bufferType,
+            final FormatSpec.FormatOptions formatOptions) {
+        results.add(runReadUnigramsAndBigramsBinary(sWords, sEmptyBigrams, bufferType,
+                formatOptions, "unigram"));
+        results.add(runReadUnigramsAndBigramsBinary(sWords, sChainBigrams, bufferType,
+                formatOptions, "chain"));
+        results.add(runReadUnigramsAndBigramsBinary(sWords, sChainBigrams, bufferType,
+                formatOptions, "star"));
+    }
+
+    public void testReadUnigramsAndBigramsBinaryWithByteBuffer() {
+        final List<String> results = CollectionUtils.newArrayList();
+
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER, VERSION2);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER, VERSION3_WITHOUT_PARENTADDRESS);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER, VERSION3_WITH_PARENTADDRESS);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER, VERSION3_WITH_LINKEDLIST_NODE);
+
+        for (final String result : results) {
+            Log.d(TAG, result);
+        }
+    }
+
+    public void testReadUnigramsAndBigramsBinaryWithByteArray() {
+        final List<String> results = CollectionUtils.newArrayList();
+
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY, VERSION2);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY, VERSION3_WITHOUT_PARENTADDRESS);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY, VERSION3_WITH_PARENTADDRESS);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY, VERSION3_WITH_LINKEDLIST_NODE);
+
+        for (final String result : results) {
+            Log.d(TAG, result);
+        }
+    }
+
+    // Tests for getTerminalPosition
+    private String getWordFromBinary(final FusionDictionaryBufferInterface buffer,
+            final int address) {
+        if (buffer.position() != 0) buffer.position(0);
+
+        FileHeader header = null;
+        try {
+            header = BinaryDictInputOutput.readHeader(buffer);
+        } catch (IOException e) {
+            return null;
+        } catch (UnsupportedFormatException e) {
+            return null;
+        }
+        if (header == null) return null;
+        return BinaryDictInputOutput.getWordAtAddress(buffer, header.mHeaderSize,
+                address - header.mHeaderSize, header.mFormatOptions);
+    }
+
+    private long runGetTerminalPosition(final FusionDictionaryBufferInterface buffer,
+            final String word, int index, boolean contained) {
+        final int expectedFrequency = (UNIGRAM_FREQ + index) % 255;
+        long diff = -1;
+        int position = -1;
+        try {
+            final long now = System.nanoTime();
+            position = BinaryDictIOUtils.getTerminalPosition(buffer, word);
+            diff = System.nanoTime() - now;
+        } catch (IOException e) {
+            Log.e(TAG, "IOException while getTerminalPosition: " + e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "UnsupportedFormatException while getTermianlPosition: " + e);
+        }
+
+        assertEquals(FormatSpec.NOT_VALID_WORD != position, contained);
+        if (contained) assertEquals(getWordFromBinary(buffer, position), word);
+        return diff;
+    }
+
+    public void testGetTerminalPosition() {
+        File file = null;
+        try {
+            file = File.createTempFile("testGetTerminalPosition", ".dict");
+        } catch (IOException e) {
+            // do nothing
+        }
+        assertNotNull(file);
+
+        final FusionDictionary dict = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(
+                        new HashMap<String, String>(), false, false));
+        addUnigrams(sWords.size(), dict, sWords, null /* shortcutMap */);
+        timeWritingDictToFile(file, dict, VERSION3_WITH_LINKEDLIST_NODE);
+
+        final FusionDictionaryBufferInterface buffer = getBuffer(file, USE_BYTE_ARRAY);
+
+        try {
+            // too long word
+            final String longWord = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
+            assertEquals(FormatSpec.NOT_VALID_WORD,
+                    BinaryDictIOUtils.getTerminalPosition(buffer, longWord));
+
+            // null
+            assertEquals(FormatSpec.NOT_VALID_WORD,
+                    BinaryDictIOUtils.getTerminalPosition(buffer, null));
+
+            // empty string
+            assertEquals(FormatSpec.NOT_VALID_WORD,
+                    BinaryDictIOUtils.getTerminalPosition(buffer, ""));
+        } catch (IOException e) {
+        } catch (UnsupportedFormatException e) {
+        }
+
+        // Test a word that is contained within the dictionary.
+        long sum = 0;
+        for (int i = 0; i < sWords.size(); ++i) {
+            final long time = runGetTerminalPosition(buffer, sWords.get(i), i, true);
+            sum += time == -1 ? 0 : time;
+        }
+        Log.d(TAG, "per a search : " + (((double)sum) / sWords.size() / 1000000));
+
+        // Test a word that isn't contained within the dictionary.
+        final Random random = new Random((int)System.currentTimeMillis());
+        for (int i = 0; i < 1000; ++i) {
+            final String word = generateWord(random.nextInt());
+            if (sWords.indexOf(word) != -1) continue;
+            runGetTerminalPosition(buffer, word, i, false);
+        }
+    }
+
+    public void testDeleteWord() {
+        File file = null;
+        try {
+            file = File.createTempFile("testGetTerminalPosition", ".dict");
+        } catch (IOException e) {
+            // do nothing
+        }
+        assertNotNull(file);
+
+        final FusionDictionary dict = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(
+                        new HashMap<String, String>(), false, false));
+        addUnigrams(sWords.size(), dict, sWords, null /* shortcutMap */);
+        timeWritingDictToFile(file, dict, VERSION3_WITH_LINKEDLIST_NODE);
+
+        final FusionDictionaryBufferInterface buffer = getBuffer(file, USE_BYTE_ARRAY);
+
+        try {
+            MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
+                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(0)));
+            BinaryDictIOUtils.deleteWord(buffer, sWords.get(0));
+            assertEquals(FormatSpec.NOT_VALID_WORD,
+                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(0)));
+
+            MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
+                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(5)));
+            BinaryDictIOUtils.deleteWord(buffer, sWords.get(5));
+            assertEquals(FormatSpec.NOT_VALID_WORD,
+                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(5)));
+        } catch (IOException e) {
+        } catch (UnsupportedFormatException e) {
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java b/tests/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java
new file mode 100644
index 0000000..21406d3
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java
@@ -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.
+ */
+
+package com.android.inputmethod.latin.spellcheck;
+
+import android.text.SpannableStringBuilder;
+import android.text.style.CharacterStyle;
+import android.text.style.SuggestionSpan;
+
+import com.android.inputmethod.latin.InputTestsBase;
+
+public class AndroidSpellCheckerServiceTest extends InputTestsBase {
+    public void testSpellchecker() {
+        mTextView.onAttachedToWindow();
+        mTextView.setText("tgis");
+        type(" ");
+        sleep(1000);
+        runMessages();
+        sleep(1000);
+
+        final SpanGetter span = new SpanGetter(mTextView.getText(), SuggestionSpan.class);
+        // If no span, the following will crash
+        final String[] suggestions = span.getSuggestions();
+        // For this test we consider "tgis" should yield at least 2 suggestions (at this moment
+        // it yields 5).
+        assertTrue(suggestions.length >= 2);
+        // We also assume the top suggestion should be "this".
+        assertEquals("", "this", suggestions[0]);
+    }
+
+    public void testRussianSpellchecker() {
+        changeLanguage("ru");
+        mTextView.onAttachedToWindow();
+        mTextView.setText("годп");
+        type(" ");
+        sleep(1000);
+        runMessages();
+        sleep(1000);
+
+        final SpanGetter span = new SpanGetter(mTextView.getText(), SuggestionSpan.class);
+        // If no span, the following will crash
+        final String[] suggestions = span.getSuggestions();
+        // For this test we consider "годп" should yield at least 2 suggestions (at this moment
+        // it yields 5).
+        assertTrue(suggestions.length >= 2);
+        // We also assume the top suggestion should be "года", which is the top word in the
+        // Russian dictionary.
+        assertEquals("", "года", suggestions[0]);
+    }
+}
diff --git a/tools/makedict/Android.mk b/tools/dicttool/Android.mk
similarity index 67%
rename from tools/makedict/Android.mk
rename to tools/dicttool/Android.mk
index 7b5dee2..5bd836a 100644
--- a/tools/makedict/Android.mk
+++ b/tools/dicttool/Android.mk
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2011 The Android Open Source Project
+# 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.
@@ -16,15 +16,18 @@
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
-MAKEDICT_CORE_SOURCE_DIRECTORY := ../../java/src/com/android/inputmethod/latin/makedict
+LATINIME_CORE_SOURCE_DIRECTORY := ../../java/src/com/android/inputmethod/latin
+MAKEDICT_CORE_SOURCE_DIRECTORY := $(LATINIME_CORE_SOURCE_DIRECTORY)/makedict
 
 LOCAL_MAIN_SRC_FILES := $(call all-java-files-under,$(MAKEDICT_CORE_SOURCE_DIRECTORY))
 LOCAL_TOOL_SRC_FILES := $(call all-java-files-under,src)
 LOCAL_SRC_FILES := $(LOCAL_TOOL_SRC_FILES) \
-        $(filter-out $(addprefix %, $(LOCAL_TOOL_SRC_FILES)), $(LOCAL_MAIN_SRC_FILES))
-LOCAL_SRC_FILES += $(call all-java-files-under,tests)
+        $(filter-out $(addprefix %/, $(notdir $(LOCAL_TOOL_SRC_FILES))), $(LOCAL_MAIN_SRC_FILES)) \
+        $(call all-java-files-under,tests) \
+        $(LATINIME_CORE_SOURCE_DIRECTORY)/Constants.java
+
 LOCAL_JAR_MANIFEST := etc/manifest.txt
-LOCAL_MODULE := makedict
+LOCAL_MODULE := dicttool_aosp
 LOCAL_JAVA_LIBRARIES := junit
 
 include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/tools/makedict/etc/Android.mk b/tools/dicttool/etc/Android.mk
similarity index 84%
rename from tools/makedict/etc/Android.mk
rename to tools/dicttool/etc/Android.mk
index 1b7d7cf..0c611b7 100644
--- a/tools/makedict/etc/Android.mk
+++ b/tools/dicttool/etc/Android.mk
@@ -1,4 +1,4 @@
-# Copyright (C) 2011 The Android Open Source Project
+# 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.
@@ -15,5 +15,5 @@
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
-LOCAL_PREBUILT_EXECUTABLES := makedict
+LOCAL_PREBUILT_EXECUTABLES := dicttool_aosp makedict_aosp
 include $(BUILD_HOST_PREBUILT)
diff --git a/tools/makedict/etc/makedict b/tools/dicttool/etc/dicttool_aosp
similarity index 90%
rename from tools/makedict/etc/makedict
rename to tools/dicttool/etc/dicttool_aosp
index 7c1c02e..a4879a2 100755
--- a/tools/makedict/etc/makedict
+++ b/tools/dicttool/etc/dicttool_aosp
@@ -33,7 +33,7 @@
 prog="${progdir}"/`basename "${prog}"`
 cd "${oldwd}"
 
-jarfile=makedict.jar
+jarfile=dicttool_aosp.jar
 frameworkdir="$progdir"
 if [ ! -r "$frameworkdir/$jarfile" ]
 then
@@ -58,6 +58,5 @@
     jarpath="$frameworkdir/$jarfile"
 fi
 
-# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
 # might need more memory, e.g. -Xmx128M
-exec java -ea -Djava.ext.dirs="$frameworkdir" -jar "$jarpath" "$@"
+exec java -ea -jar "$jarpath" "$@"
diff --git a/tools/makedict/etc/Android.mk b/tools/dicttool/etc/makedict_aosp
old mode 100644
new mode 100755
similarity index 63%
copy from tools/makedict/etc/Android.mk
copy to tools/dicttool/etc/makedict_aosp
index 1b7d7cf..095c505
--- a/tools/makedict/etc/Android.mk
+++ b/tools/dicttool/etc/makedict_aosp
@@ -1,10 +1,11 @@
-# Copyright (C) 2011 The Android Open Source Project
+#!/bin/sh
+# 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
+#     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,
@@ -12,8 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_PREBUILT_EXECUTABLES := makedict
-include $(BUILD_HOST_PREBUILT)
+# Dicttool supports making the dictionary using the 'makedict' command and
+# the same arguments that the old 'makedict' command used to accept.
+dicttool_aosp makedict $@
diff --git a/tools/dicttool/etc/manifest.txt b/tools/dicttool/etc/manifest.txt
new file mode 100644
index 0000000..67c8521
--- /dev/null
+++ b/tools/dicttool/etc/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.inputmethod.latin.dicttool.Dicttool
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/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
new file mode 100644
index 0000000..3cb0a12
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Compress.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.dicttool;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+public class Compress {
+
+    private static OutputStream getCompressedStream(final OutputStream out)
+        throws java.io.IOException {
+        return new GZIPOutputStream(out);
+    }
+
+    private static InputStream getUncompressedStream(final InputStream in) throws IOException {
+        return new GZIPInputStream(in);
+    }
+
+    public static void copy(final InputStream input, final OutputStream output) throws IOException {
+        final byte[] buffer = new byte[1000];
+        for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
+            output.write(buffer, 0, readBytes);
+        input.close();
+        output.close();
+    }
+
+    static public class Compressor extends Dicttool.Command {
+        public static final String COMMAND = "compress";
+        public static final String STDIN_OR_STDOUT = "-";
+
+        public Compressor() {
+        }
+
+        public String getHelp() {
+            return COMMAND + " <src_filename> <dst_filename>: "
+                    + "Compresses a file using gzip compression";
+        }
+
+        public void run() throws IOException {
+            if (mArgs.length > 2) {
+                throw new RuntimeException("Too many arguments for command " + COMMAND);
+            }
+            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";
+        public static final String STDIN_OR_STDOUT = "-";
+
+        public Uncompressor() {
+        }
+
+        public String getHelp() {
+            return COMMAND + " <src_filename> <dst_filename>: "
+                    + "Uncompresses a file compressed with gzip compression";
+        }
+
+        public void run() throws IOException {
+            if (mArgs.length > 2) {
+                throw new RuntimeException("Too many arguments for command " + COMMAND);
+            }
+            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/makedict/src/com/android/inputmethod/latin/makedict/DictionaryMaker.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/DictionaryMaker.java
similarity index 81%
rename from tools/makedict/src/com/android/inputmethod/latin/makedict/DictionaryMaker.java
rename to tools/dicttool/src/android/inputmethod/latin/dicttool/DictionaryMaker.java
index 5e39215..4f88749 100644
--- a/tools/makedict/src/com/android/inputmethod/latin/makedict/DictionaryMaker.java
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/DictionaryMaker.java
@@ -14,7 +14,13 @@
  * the License.
  */
 
-package com.android.inputmethod.latin.makedict;
+package com.android.inputmethod.latin.dicttool;
+
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FormatSpec;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.MakedictLog;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -22,7 +28,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;
 
@@ -36,40 +43,35 @@
 public class DictionaryMaker {
 
     static class Arguments {
-        private final static String OPTION_VERSION_2 = "-2";
-        private final static String OPTION_INPUT_SOURCE = "-s";
-        private final static String OPTION_INPUT_BIGRAM_XML = "-b";
-        private final static String OPTION_INPUT_SHORTCUT_XML = "-c";
-        private final static String OPTION_OUTPUT_BINARY = "-d";
-        private final static String OPTION_OUTPUT_BINARY_FORMAT_VERSION_1 = "-d1";
-        private final static String OPTION_OUTPUT_XML = "-x";
-        private final static String OPTION_HELP = "-h";
+        private static final String OPTION_VERSION_1 = "-1";
+        private static final String OPTION_VERSION_2 = "-2";
+        private static final String OPTION_VERSION_3 = "-3";
+        private static final String OPTION_INPUT_SOURCE = "-s";
+        private static final String OPTION_INPUT_BIGRAM_XML = "-b";
+        private static final String OPTION_INPUT_SHORTCUT_XML = "-c";
+        private static final String OPTION_OUTPUT_BINARY = "-d";
+        private static final String OPTION_OUTPUT_XML = "-x";
+        private static final String OPTION_HELP = "-h";
         public final String mInputBinary;
         public final String mInputUnigramXml;
         public final String mInputShortcutXml;
         public final String mInputBigramXml;
         public final String mOutputBinary;
-        public final String mOutputBinaryFormat1;
         public final String mOutputXml;
+        public final int mOutputBinaryFormatVersion;
 
         private void checkIntegrity() throws IOException {
             checkHasExactlyOneInput();
             checkHasAtLeastOneOutput();
             checkNotSameFile(mInputBinary, mOutputBinary);
-            checkNotSameFile(mInputBinary, mOutputBinaryFormat1);
             checkNotSameFile(mInputBinary, mOutputXml);
             checkNotSameFile(mInputUnigramXml, mOutputBinary);
-            checkNotSameFile(mInputUnigramXml, mOutputBinaryFormat1);
             checkNotSameFile(mInputUnigramXml, mOutputXml);
             checkNotSameFile(mInputShortcutXml, mOutputBinary);
-            checkNotSameFile(mInputShortcutXml, mOutputBinaryFormat1);
             checkNotSameFile(mInputShortcutXml, mOutputXml);
             checkNotSameFile(mInputBigramXml, mOutputBinary);
-            checkNotSameFile(mInputBigramXml, mOutputBinaryFormat1);
             checkNotSameFile(mInputBigramXml, mOutputXml);
-            checkNotSameFile(mOutputBinary, mOutputBinaryFormat1);
             checkNotSameFile(mOutputBinary, mOutputXml);
-            checkNotSameFile(mOutputBinaryFormat1, mOutputXml);
         }
 
         private void checkHasExactlyOneInput() {
@@ -84,7 +86,7 @@
         }
 
         private void checkHasAtLeastOneOutput() {
-            if (null == mOutputBinary && null == mOutputBinaryFormat1 && null == mOutputXml) {
+            if (null == mOutputBinary && null == mOutputXml) {
                 throw new RuntimeException("No output specified");
             }
         }
@@ -102,19 +104,23 @@
         }
 
         private void displayHelp() {
-            MakedictLog.i("Usage: makedict "
-                    + "[-s <unigrams.xml> [-b <bigrams.xml>] [-c <shortcuts.xml>] "
-                    + "| -s <binary input>] [-d <binary output format version 2>] "
-                    + "[-d1 <binary output format version 1>] [-x <xml output>] [-2]\n"
+            MakedictLog.i(getHelp());
+        }
+
+        public static String getHelp() {
+            return "Usage: makedict "
+                    + "[-s <unigrams.xml> [-b <bigrams.xml>] [-c <shortcuts_and_whitelist.xml>] "
+                    + "| [-s <binary input>] [-d <binary output>] [-x <xml output>] "
+                    + "[-1] [-2] [-3]\n"
                     + "\n"
                     + "  Converts a source dictionary file to one or several outputs.\n"
                     + "  Source can be an XML file, with an optional XML bigrams file, or a\n"
                     + "  binary dictionary file.\n"
-                    + "  Binary version 1 (Ice Cream Sandwich), 2 (Jelly Bean) and XML outputs\n"
+                    + "  Binary version 1 (Ice Cream Sandwich), 2 (Jelly Bean), 3 and XML outputs\n"
                     + "  are supported. All three can be output at the same time, but the same\n"
                     + "  output format cannot be specified several times. The behavior is\n"
                     + "  unspecified if the same file is specified for input and output, or for\n"
-                    + "  several outputs.");
+                    + "  several outputs.";
         }
 
         public Arguments(String[] argsArray) throws IOException {
@@ -127,8 +133,8 @@
             String inputShortcutXml = null;
             String inputBigramXml = null;
             String outputBinary = null;
-            String outputBinaryFormat1 = null;
             String outputXml = null;
+            int outputBinaryFormatVersion = 2; // the default version is 2.
 
             while (!args.isEmpty()) {
                 final String arg = args.get(0);
@@ -136,6 +142,10 @@
                 if (arg.charAt(0) == '-') {
                     if (OPTION_VERSION_2.equals(arg)) {
                         // Do nothing, this is the default
+                    } else if (OPTION_VERSION_3.equals(arg)) {
+                        outputBinaryFormatVersion = 3;
+                    } else if (OPTION_VERSION_1.equals(arg)) {
+                        outputBinaryFormatVersion = 1;
                     } else if (OPTION_HELP.equals(arg)) {
                         displayHelp();
                     } else {
@@ -158,8 +168,6 @@
                             inputBigramXml = filename;
                         } else if (OPTION_OUTPUT_BINARY.equals(arg)) {
                             outputBinary = filename;
-                        } else if (OPTION_OUTPUT_BINARY_FORMAT_VERSION_1.equals(arg)) {
-                            outputBinaryFormat1 = filename;
                         } else if (OPTION_OUTPUT_XML.equals(arg)) {
                             outputXml = filename;
                         } else {
@@ -186,8 +194,8 @@
             mInputShortcutXml = inputShortcutXml;
             mInputBigramXml = inputBigramXml;
             mOutputBinary = outputBinary;
-            mOutputBinaryFormat1 = outputBinaryFormat1;
             mOutputXml = outputXml;
+            mOutputBinaryFormatVersion = outputBinaryFormatVersion;
             checkIntegrity();
         }
     }
@@ -229,15 +237,31 @@
      */
     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(
+                    new BinaryDictInputOutput.ByteBufferWrapper(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
@@ -269,10 +293,7 @@
             throws FileNotFoundException, IOException, UnsupportedFormatException,
             IllegalArgumentException {
         if (null != args.mOutputBinary) {
-            writeBinaryDictionary(args.mOutputBinary, dict, 2);
-        }
-        if (null != args.mOutputBinaryFormat1) {
-            writeBinaryDictionary(args.mOutputBinaryFormat1, dict, 1);
+            writeBinaryDictionary(args.mOutputBinary, dict, args.mOutputBinaryFormatVersion);
         }
         if (null != args.mOutputXml) {
             writeXmlDictionary(args.mOutputXml, dict);
@@ -292,8 +313,9 @@
             final FusionDictionary dict, final int version)
             throws FileNotFoundException, IOException, UnsupportedFormatException {
         final File outputFile = new File(outputFilename);
+        final FormatSpec.FormatOptions formatOptions = new FormatSpec.FormatOptions(version);
         BinaryDictInputOutput.writeDictionaryBinary(new FileOutputStream(outputFilename), dict,
-                version);
+                formatOptions);
     }
 
     /**
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java
new file mode 100644
index 0000000..bf417fb
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Dicttool.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.dicttool;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class Dicttool {
+
+    public static abstract class Command {
+        protected String[] mArgs;
+        public void setArgs(String[] args) throws IllegalArgumentException {
+            mArgs = args;
+        }
+        abstract public String getHelp();
+        abstract public void run() throws Exception;
+    }
+    static HashMap<String, Class<? extends Command>> sCommands =
+            new HashMap<String, Class<? extends Command>>();
+    static {
+        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) {
+        try {
+            return sCommands.get(commandName).newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(commandName + " is not installed");
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(commandName + " is not installed");
+        }
+    }
+
+    private static void help() {
+        System.out.println("Syntax: dicttool <command [arguments]>\nAvailable commands:\n");
+        for (final String commandName : sCommands.keySet()) {
+            System.out.println("*** " + commandName);
+            System.out.println(getCommandInstance(commandName).getHelp());
+            System.out.println("");
+        }
+    }
+
+    private static boolean isCommand(final String commandName) {
+        return sCommands.containsKey(commandName);
+    }
+
+    private Command getCommand(final String[] arguments) {
+        final String commandName = arguments[0];
+        if (!isCommand(commandName)) {
+            throw new RuntimeException("Unknown command : " + commandName);
+        }
+        final Command command = getCommandInstance(commandName);
+        final String[] argsArray = Arrays.copyOfRange(arguments, 1, arguments.length);
+        command.setArgs(argsArray);
+        return command;
+    }
+
+    private void execute(final String[] arguments) {
+        final Command command = getCommand(arguments);
+        try {
+            command.run();
+        } catch (Exception e) {
+            System.out.println("Exception while processing command "
+                    + command.getClass().getSimpleName() + " : " + e);
+            return;
+        }
+    }
+
+    public static void main(final String[] arguments) {
+        if (0 == arguments.length) {
+            help();
+            return;
+        }
+        new Dicttool().execute(arguments);
+    }
+}
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Info.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Info.java
new file mode 100644
index 0000000..e592617
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Info.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.dicttool;
+
+public class Info extends Dicttool.Command {
+    public static final String COMMAND = "info";
+
+    public Info() {
+    }
+
+    public String getHelp() {
+        return "info <filename>: prints various information about a dictionary file";
+    }
+
+    public void run() {
+        // TODO: implement this
+        if (mArgs.length < 1) {
+            throw new RuntimeException("Not enough arguments for command " + COMMAND);
+        }
+        System.out.println("Not implemented yet");
+    }
+}
diff --git a/tools/dicttool/src/android/inputmethod/latin/dicttool/Makedict.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/Makedict.java
new file mode 100644
index 0000000..c004cfb
--- /dev/null
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/Makedict.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.dicttool;
+
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+
+public class Makedict extends Dicttool.Command {
+    public static final String COMMAND = "makedict";
+
+    public Makedict() {
+    }
+
+    public String getHelp() {
+        return DictionaryMaker.Arguments.getHelp();
+    }
+
+    public void run() throws FileNotFoundException, IOException, ParserConfigurationException,
+            SAXException, UnsupportedFormatException {
+        DictionaryMaker.main(mArgs);
+    }
+}
diff --git a/tools/makedict/src/com/android/inputmethod/latin/makedict/MakedictLog.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/MakedictLog.java
similarity index 100%
rename from tools/makedict/src/com/android/inputmethod/latin/makedict/MakedictLog.java
rename to tools/dicttool/src/android/inputmethod/latin/dicttool/MakedictLog.java
diff --git a/tools/makedict/src/com/android/inputmethod/latin/makedict/XmlDictInputOutput.java b/tools/dicttool/src/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
similarity index 84%
rename from tools/makedict/src/com/android/inputmethod/latin/makedict/XmlDictInputOutput.java
rename to tools/dicttool/src/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
index 52f124d..c31cd72 100644
--- a/tools/makedict/src/com/android/inputmethod/latin/makedict/XmlDictInputOutput.java
+++ b/tools/dicttool/src/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
@@ -14,11 +14,13 @@
  * the License.
  */
 
-package com.android.inputmethod.latin.makedict;
+package com.android.inputmethod.latin.dicttool;
 
+import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.makedict.Word;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -48,6 +50,7 @@
     private static final String SHORTCUT_TAG = "shortcut";
     private static final String FREQUENCY_ATTR = "f";
     private static final String WORD_ATTR = "word";
+    private static final String NOT_A_WORD_ATTR = "not_a_word";
 
     private static final int SHORTCUT_ONLY_DEFAULT_FREQ = 1;
 
@@ -88,6 +91,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), true /* isNotAWord */);
+            }
             mDictionary = null;
             mShortcutsMap.clear();
             mWord = "";
@@ -138,7 +145,7 @@
         @Override
         public void endElement(String uri, String localName, String qName) {
             if (WORD == mState) {
-                mDictionary.add(mWord, mFreq, mShortcutsMap.get(mWord));
+                mDictionary.add(mWord, mFreq, mShortcutsMap.get(mWord), false /* isNotAWord */);
                 mState = START;
             }
         }
@@ -177,7 +184,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>();
@@ -186,6 +193,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;
@@ -214,22 +225,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();
         }
     }
@@ -241,7 +270,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.
      */
@@ -254,11 +283,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();
@@ -278,7 +308,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.
      *
@@ -289,7 +319,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.
@@ -316,7 +346,8 @@
         destination.write("<!-- Warning: there is no code to read this format yet. -->\n");
         for (Word word : set) {
             destination.write("  <" + WORD_TAG + " " + WORD_ATTR + "=\"" + word.mWord + "\" "
-                    + FREQUENCY_ATTR + "=\"" + word.mFrequency + "\">");
+                    + FREQUENCY_ATTR + "=\"" + word.mFrequency
+                    + (word.mIsNotAWord ? "\" " + NOT_A_WORD_ATTR + "=\"true" : "") + "\">");
             if (null != word.mShortcutTargets) {
                 destination.write("\n");
                 for (WeightedString target : word.mShortcutTargets) {
diff --git a/tools/makedict/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java b/tools/dicttool/tests/com/android/inputmethod/latin/makedict/BinaryDictInputOutputTest.java
similarity index 86%
rename from tools/makedict/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java
rename to tools/dicttool/tests/com/android/inputmethod/latin/makedict/BinaryDictInputOutputTest.java
index 24042f1..88589b8 100644
--- a/tools/makedict/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/makedict/BinaryDictInputOutputTest.java
@@ -43,11 +43,11 @@
         final FusionDictionary dict = new FusionDictionary(new Node(),
                 new DictionaryOptions(new HashMap<String, String>(),
                         false /* germanUmlautProcessing */, false /* frenchLigatureProcessing */));
-        dict.add("foo", 1, null);
-        dict.add("fta", 1, null);
-        dict.add("ftb", 1, null);
-        dict.add("bar", 1, null);
-        dict.add("fool", 1, null);
+        dict.add("foo", 1, null, false /* isNotAWord */);
+        dict.add("fta", 1, null, false /* isNotAWord */);
+        dict.add("ftb", 1, null, false /* isNotAWord */);
+        dict.add("bar", 1, null, false /* isNotAWord */);
+        dict.add("fool", 1, null, false /* isNotAWord */);
         final ArrayList<Node> result = BinaryDictInputOutput.flattenTree(dict.mRoot);
         assertEquals(4, result.size());
         while (!result.isEmpty()) {
diff --git a/tools/dicttool/tests/etc/test-dicttool.sh b/tools/dicttool/tests/etc/test-dicttool.sh
new file mode 100755
index 0000000..8834611
--- /dev/null
+++ b/tools/dicttool/tests/etc/test-dicttool.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# 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.
+
+java -classpath ${ANDROID_HOST_OUT}/framework/junit.jar:${ANDROID_HOST_OUT}/../common/obj/JAVA_LIBRARIES/dicttool_intermediates/classes junit.textui.TestRunner com.android.inputmethod.latin.makedict.BinaryDictInputOutputTest
diff --git a/tools/makedict/etc/manifest.txt b/tools/makedict/etc/manifest.txt
deleted file mode 100644
index 4f085e7..0000000
--- a/tools/makedict/etc/manifest.txt
+++ /dev/null
@@ -1 +0,0 @@
-Main-Class: com.android.inputmethod.latin.makedict.DictionaryMaker
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);
diff --git a/tools/maketext/res/values-af/donottranslate-more-keys.xml b/tools/maketext/res/values-af/donottranslate-more-keys.xml
new file mode 100644
index 0000000..ee96f44
--- /dev/null
+++ b/tools/maketext/res/values-af/donottranslate-more-keys.xml
@@ -0,0 +1,69 @@
+<?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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- This is the same as Dutch except more keys of y and demoting vowels with diaeresis. -->
+    <!-- U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+         U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+         U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+         U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+         U+00E6: "æ" LATIN SMALL LETTER AE
+         U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+         U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+         U+0101: "ā" LATIN SMALL LETTER A WITH MACRON -->
+    <string name="more_keys_for_a">&#x00E1;,&#x00E2;,&#x00E4;,&#x00E0;,&#x00E6;,&#x00E3;,&#x00E5;,&#x0101;</string>
+    <!-- U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+         U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+         U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+         U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+         U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+         U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+         U+0113: "ē" LATIN SMALL LETTER E WITH MACRON -->
+    <string name="more_keys_for_e">&#x00E9;,&#x00E8;,&#x00EA;,&#x00EB;,&#x0119;,&#x0117;,&#x0113;</string>
+    <!-- U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+         U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+         U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+         U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+         U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+         U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+         U+0133: "ij" LATIN SMALL LIGATURE IJ -->
+    <string name="more_keys_for_i">&#x00ED;,&#x00EC;,&#x00EF;,&#x00EE;,&#x012F;,&#x012B;,&#x0133;</string>
+    <!-- U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+         U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+         U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+         U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+         U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+         U+0153: "œ" LATIN SMALL LIGATURE OE
+         U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+         U+014D: "ō" LATIN SMALL LETTER O WITH MACRON -->
+    <string name="more_keys_for_o">&#x00F3;,&#x00F4;,&#x00F6;,&#x00F2;,&#x00F5;,&#x0153;,&#x00F8;,&#x014D;</string>
+    <!-- U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+         U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+         U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+         U+016B: "ū" LATIN SMALL LETTER U WITH MACRON -->
+    <string name="more_keys_for_u">&#x00FA;,&#x00FB;,&#x00FC;,&#x00F9;,&#x016B;</string>
+    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+         U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE -->
+    <string name="more_keys_for_n">&#x00F1;,&#x0144;</string>
+    <string name="more_keys_for_y">&#x00FD;,&#x0177;,&#x00FF;,&#x0133;</string>
+    <!-- U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+         U+0133: "ij" LATIN SMALL LIGATURE IJ -->
+    <string name="more_keys_for_y">&#x00FD;,&#x0133;</string>
+</resources>
diff --git a/tools/maketext/res/values-be/donottranslate-more-keys.xml b/tools/maketext/res/values-be/donottranslate-more-keys.xml
index 835553a..a2056e9 100644
--- a/tools/maketext/res/values-be/donottranslate-more-keys.xml
+++ b/tools/maketext/res/values-be/donottranslate-more-keys.xml
@@ -20,12 +20,16 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- U+045E: "ў" CYRILLIC SMALL LETTER SHORT U -->
     <string name="keylabel_for_east_slavic_row1_9">&#x045E;</string>
+    <!-- U+0451: "ё" CYRILLIC SMALL LETTER IO -->
+    <string name="keylabel_for_east_slavic_row1_12">&#x0451;</string>
     <!-- U+044B: "ы" CYRILLIC SMALL LETTER YERU -->
     <string name="keylabel_for_east_slavic_row2_1">&#x044B;</string>
+    <!-- U+044D: "э" CYRILLIC SMALL LETTER E -->
+    <string name="keylabel_for_east_slavic_row2_11">&#x044D;</string>
     <!-- U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I -->
     <string name="keylabel_for_east_slavic_row3_5">&#x0456;</string>
-    <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
-    <string name="more_keys_for_cyrillic_ha">&#x044A;</string>
+    <!-- U+0451: "ё" CYRILLIC SMALL LETTER IO -->
+    <string name="more_keys_for_cyrillic_ie">&#x0451;</string>
     <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
     <string name="more_keys_for_cyrillic_soft_sign">&#x044A;</string>
 </resources>
diff --git a/tools/maketext/res/values-eo/donottranslate-more-keys.xml b/tools/maketext/res/values-eo/donottranslate-more-keys.xml
new file mode 100644
index 0000000..e929869
--- /dev/null
+++ b/tools/maketext/res/values-eo/donottranslate-more-keys.xml
@@ -0,0 +1,146 @@
+<?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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+         U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+         U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+         U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+         U+00E6: "æ" LATIN SMALL LETTER AE
+         U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+         U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+         U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+         U+0103: "ă" LATIN SMALL LETTER A WITH BREVE
+         U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+         U+00AA: "ª" FEMININE ORDINAL INDICATOR -->
+    <string name="more_keys_for_a">&#x00E1;,&#x00E0;,&#x00E2;,&#x00E4;,&#x00E6;,&#x00E3;,&#x00E5;,&#x0101;,&#x0103;,&#x0105;,&#x00AA;</string>
+    <!-- U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+         U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+         U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+         U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+         U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+         U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+         U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+         U+0113: "ē" LATIN SMALL LETTER E WITH MACRON -->
+    <string name="more_keys_for_e">&#x00E9;,&#x011B;,&#x00E8;,&#x00EA;,&#x00EB;,&#x0119;,&#x0117;,&#x0113;</string>
+    <!-- U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+         U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+         U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+         U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE
+         U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+         U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+         U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+         U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+         U+0133: "ij" LATIN SMALL LIGATURE IJ -->
+    <string name="more_keys_for_i">&#x00ED;,&#x00EE;,&#x00EF;,&#x0129;,&#x00EC;,&#x012F;,&#x012B;,&#x0131;,&#x0133;</string>
+    <!-- U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+         U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+         U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+         U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+         U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+         U+0153: "œ" LATIN SMALL LIGATURE OE
+         U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+         U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+         U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+         U+00BA: "º" MASCULINE ORDINAL INDICATOR -->
+    <string name="more_keys_for_o">&#x00F3;,&#x00F6;,&#x00F4;,&#x00F2;,&#x00F5;,&#x0153;,&#x00F8;,&#x014D;,&#x0151;,&#x00BA;</string>
+    <!-- U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+         U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+         U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+         U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+         U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+         U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE
+         U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+         U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+         U+00B5: "µ" MICRO SIGN -->
+    <string name="more_keys_for_u">&#x00FA;,&#x016F;,&#x00FB;,&#x00FC;,&#x00F9;,&#x016B;,&#x0169;,&#x0171;,&#x0173;,&#x00B5;</string>
+    <!-- U+00DF: "ß" LATIN SMALL LETTER SHARP S
+         U+0161: "š" LATIN SMALL LETTER S WITH CARON
+         U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+         U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW
+         U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA -->
+    <string name="more_keys_for_s">&#x00DF;,&#x0161;,&#x015B;,&#x0219;,&#x015F;</string>
+    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+         U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+         U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+         U+0148: "ň" LATIN SMALL LETTER N WITH CARON
+         U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
+         U+014B: "ŋ" LATIN SMALL LETTER ENG -->
+    <string name="more_keys_for_n">&#x00F1;,&#x0144;,&#x0146;,&#x0148;,&#x0149;,&#x014B;</string>
+    <!-- U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+         U+010D: "č" LATIN SMALL LETTER C WITH CARON
+         U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+         U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE -->
+    <string name="more_keys_for_c">&#x0107;,&#x010D;,&#x00E7;,&#x010B;</string>
+    <!-- U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+         U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX
+         U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+         U+00FE: "þ" LATIN SMALL LETTER THORN -->
+    <string name="more_keys_for_y">y,&#x00FD;,&#x0177;,&#x00FF;,&#x00FE;</string>
+    <!-- U+00F0: "ð" LATIN SMALL LETTER ETH
+         U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+         U+0111: "đ" LATIN SMALL LETTER D WITH STROKE -->
+    <string name="more_keys_for_d">&#x00F0;,&#x010F;,&#x0111;</string>
+    <!-- U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+         U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+         U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA  -->
+    <string name="more_keys_for_r">&#x0159;,&#x0155;,&#x0157;</string>
+    <!-- U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+         U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW
+         U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+         U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE -->
+    <string name="more_keys_for_t">&#x0165;,&#x021B;,&#x0163;,&#x0167;</string>
+    <!-- U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+         U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+         U+017E: "ž" LATIN SMALL LETTER Z WITH CARON -->
+    <string name="more_keys_for_z">&#x017A;,&#x017C;,&#x017E;</string>
+    <!-- U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+         U+0138: "ĸ" LATIN SMALL LETTER KRA  -->
+    <string name="more_keys_for_k">&#x0137;,&#x0138;</string>
+    <!-- U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+         U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+         U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+         U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT
+         U+0142: "ł" LATIN SMALL LETTER L WITH STROKE -->
+    <string name="more_keys_for_l">&#x013A;,&#x013C;,&#x013E;,&#x0140;,&#x0142;</string>
+    <!-- U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+         U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE
+         U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA -->
+    <string name="more_keys_for_g">&#x011F;,&#x0121;,&#x0123;</string>
+    <!-- U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX -->
+    <string name="more_keys_for_v">w,&#x0175;</string>
+    <!-- U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX
+         U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE -->
+    <string name="more_keys_for_h">&#x0125;,&#x0127;</string>
+    <!-- U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX -->
+    <string name="more_keys_for_w">w,&#x0175;</string>
+    <string name="more_keys_for_q">q</string>
+    <string name="more_keys_for_x">x</string>
+    <!-- U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX -->
+    <string name="keylabel_for_q">&#x015D;</string>
+    <!-- U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX -->
+    <string name="keylabel_for_w">&#x011D;</string>
+    <!-- U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE -->
+    <string name="keylabel_for_y">&#x016D;</string>
+    <!-- U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX -->
+    <string name="keylabel_for_x">&#x0109;</string>
+    <!-- U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX -->
+    <string name="keylabel_for_spanish_row2_10">&#x0135;</string>
+</resources>
diff --git a/tools/maketext/res/values-es/donottranslate-more-keys.xml b/tools/maketext/res/values-es/donottranslate-more-keys.xml
index 2077af4..586dbad 100644
--- a/tools/maketext/res/values-es/donottranslate-more-keys.xml
+++ b/tools/maketext/res/values-es/donottranslate-more-keys.xml
@@ -69,15 +69,9 @@
     <string name="more_keys_for_c">&#x00E7;,&#x0107;,&#x010D;</string>
     <!-- U+00A1: "¡" INVERTED EXCLAMATION MARK
          U+00BF: "¿" INVERTED QUESTION MARK -->
-    <string name="more_keys_for_punctuation">"!fixedColumnOrder!9,\",\',#,-,&#x00A1;,!,&#x00BF;,\\,,\?,\@,&amp;,\\%,+,;,:,/,(,)"</string>
-    <string name="keyhintlabel_for_tablet_comma">&#x00A1;</string>
-    <string name="more_keys_for_tablet_comma">"&#x00A1;,!"</string>
-    <string name="keyhintlabel_for_tablet_period">&#x00BF;</string>
-    <string name="more_keys_for_tablet_period">"&#x00BF;,\?"</string>
+    <string name="more_keys_for_punctuation">"!fixedColumnOrder!9,&#x00A1;,\",\',#,-,:,!,\\,,\?,&#x00BF;,\@,&amp;,\\%,+,;,/,(,)"</string>
     <!-- U+00A1: "¡" INVERTED EXCLAMATION MARK -->
-    <string name="keylabel_for_symbols_exclamation">"&#x00A1;"</string>
+    <string name="more_keys_for_tablet_comma">"!,&#x00A1;"</string>
     <!-- U+00BF: "¿" INVERTED QUESTION MARK -->
-    <string name="keylabel_for_symbols_question">"&#x00BF;"</string>
-    <string name="more_keys_for_symbols_exclamation">"!"</string>
-    <string name="more_keys_for_symbols_question">"\?"</string>
+    <string name="more_keys_for_tablet_period">"\?,&#x00BF;"</string>
 </resources>
diff --git a/tools/maketext/res/values-ky/donottranslate-more-keys.xml b/tools/maketext/res/values-ky/donottranslate-more-keys.xml
index fd90248..a11ecf9 100644
--- a/tools/maketext/res/values-ky/donottranslate-more-keys.xml
+++ b/tools/maketext/res/values-ky/donottranslate-more-keys.xml
@@ -20,16 +20,20 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- U+0449: "щ" CYRILLIC SMALL LETTER SHCHA -->
     <string name="keylabel_for_east_slavic_row1_9">&#x0449;</string>
+    <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
+    <string name="keylabel_for_east_slavic_row1_12">&#x044A;</string>
     <!-- U+044B: "ы" CYRILLIC SMALL LETTER YERU -->
     <string name="keylabel_for_east_slavic_row2_1">&#x044B;</string>
+    <!-- U+044D: "э" CYRILLIC SMALL LETTER E -->
+    <string name="keylabel_for_east_slavic_row2_11">&#x044D;</string>
     <!-- U+0438: "и" CYRILLIC SMALL LETTER I -->
     <string name="keylabel_for_east_slavic_row3_5">&#x0438;</string>
     <!-- U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U -->
     <string name="more_keys_for_cyrillic_u">&#x04AF;</string>
+    <!-- U+0451: "ё" CYRILLIC SMALL LETTER IO -->
+    <string name="more_keys_for_cyrillic_ie">&#x0451;</string>
     <!-- U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER -->
     <string name="more_keys_for_cyrillic_en">&#x04A3;</string>
-    <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
-    <string name="more_keys_for_cyrillic_ha">&#x044A;</string>
     <!-- U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O -->
     <string name="more_keys_for_cyrillic_o">&#x04E9;</string>
     <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
diff --git a/tools/maketext/res/values-ru/donottranslate-more-keys.xml b/tools/maketext/res/values-ru/donottranslate-more-keys.xml
index 0bb5707..82bed78 100644
--- a/tools/maketext/res/values-ru/donottranslate-more-keys.xml
+++ b/tools/maketext/res/values-ru/donottranslate-more-keys.xml
@@ -20,14 +20,16 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- U+0449: "щ" CYRILLIC SMALL LETTER SHCHA -->
     <string name="keylabel_for_east_slavic_row1_9">&#x0449;</string>
+    <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
+    <string name="keylabel_for_east_slavic_row1_12">&#x044A;</string>
     <!-- U+044B: "ы" CYRILLIC SMALL LETTER YERU -->
     <string name="keylabel_for_east_slavic_row2_1">&#x044B;</string>
+    <!-- U+044D: "э" CYRILLIC SMALL LETTER E -->
+    <string name="keylabel_for_east_slavic_row2_11">&#x044D;</string>
     <!-- U+0438: "и" CYRILLIC SMALL LETTER I -->
     <string name="keylabel_for_east_slavic_row3_5">&#x0438;</string>
     <!-- U+0451: "ё" CYRILLIC SMALL LETTER IO -->
-    <string name="more_keys_for_cyrillic_ye">&#x0451;</string>
-    <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
-    <string name="more_keys_for_cyrillic_ha">&#x044A;</string>
+    <string name="more_keys_for_cyrillic_ie">&#x0451;</string>
     <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
     <string name="more_keys_for_cyrillic_soft_sign">&#x044A;</string>
 </resources>
diff --git a/tools/maketext/res/values-sr/donottranslate-more-keys.xml b/tools/maketext/res/values-sr/donottranslate-more-keys.xml
index e85d3d7..dcf0e85 100644
--- a/tools/maketext/res/values-sr/donottranslate-more-keys.xml
+++ b/tools/maketext/res/values-sr/donottranslate-more-keys.xml
@@ -18,6 +18,24 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified.
+         BEGIN: More keys definitions for Serbian (Latin)
+         U+0161: "š" LATIN SMALL LETTER S WITH CARON
+         U+00DF: "ß" LATIN SMALL LETTER SHARP S
+         U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+    <string name="more_keys_for_s">&#x0161;,&#x00DF;,&#x015B;</string>
+         U+010D: "č" LATIN SMALL LETTER C WITH CARON
+         U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+         U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+    <string name="more_keys_for_c">&#x010D;,&#x00E7;,&#x0107;</string>
+         U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+    <string name="more_keys_for_d">&#x010F;</string>
+         U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+         U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+         U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+    <string name="more_keys_for_z">&#x017E;,&#x017A;,&#x017C;</string>
+         END: More keys definitions for Serbian (Latin) -->
+    <!-- BEGIN: More keys definitions for Serbian (Cyrillic) -->
     <!-- U+0437: "з" CYRILLIC SMALL LETTER ZE -->
     <string name="keylabel_for_south_slavic_row1_6">&#x0437;</string>
     <!-- U+045B: "ћ" CYRILLIC SMALL LETTER TSHE -->
@@ -30,6 +48,7 @@
     <string name="more_keys_for_cyrillic_ie">&#x0450;</string>
     <!-- U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE -->
     <string name="more_keys_for_cyrillic_i">&#x045D;</string>
+    <!-- END: More keys definitions for Serbian (Cyrillic) -->
     <!-- U+2018: "‘" LEFT SINGLE QUOTATION MARK
          U+2019: "’" RIGHT SINGLE QUOTATION MARK
          U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
diff --git a/tools/maketext/res/values-sw/donottranslate-more-keys.xml b/tools/maketext/res/values-sw/donottranslate-more-keys.xml
new file mode 100644
index 0000000..968a80c
--- /dev/null
+++ b/tools/maketext/res/values-sw/donottranslate-more-keys.xml
@@ -0,0 +1,65 @@
+<?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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- This is the same as English except more_keys_for_g. -->
+    <!-- U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+         U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+         U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+         U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+         U+00E6: "æ" LATIN SMALL LETTER AE
+         U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+         U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+         U+0101: "ā" LATIN SMALL LETTER A WITH MACRON -->
+    <string name="more_keys_for_a">&#x00E0;,&#x00E1;,&#x00E2;,&#x00E4;,&#x00E6;,&#x00E3;,&#x00E5;,&#x0101;</string>
+    <!-- U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+         U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+         U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+         U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+         U+0113: "ē" LATIN SMALL LETTER E WITH MACRON -->
+    <string name="more_keys_for_e">&#x00E8;,&#x00E9;,&#x00EA;,&#x00EB;,&#x0113;</string>
+    <!-- U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+         U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+         U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+         U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+         U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE -->
+    <string name="more_keys_for_i">&#x00EE;,&#x00EF;,&#x00ED;,&#x012B;,&#x00EC;</string>
+    <!-- U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+         U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+         U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+         U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+         U+0153: "œ" LATIN SMALL LIGATURE OE
+         U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+         U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+         U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE -->
+    <string name="more_keys_for_o">&#x00F4;,&#x00F6;,&#x00F2;,&#x00F3;,&#x0153;,&#x00F8;,&#x014D;,&#x00F5;</string>
+    <!-- U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+         U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+         U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+         U+016B: "ū" LATIN SMALL LETTER U WITH MACRON -->
+    <string name="more_keys_for_u">&#x00FB;,&#x00FC;,&#x00F9;,&#x00FA;,&#x016B;</string>
+    <!-- U+00DF: "ß" LATIN SMALL LETTER SHARP S -->
+    <string name="more_keys_for_s">&#x00DF;</string>
+    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE -->
+    <string name="more_keys_for_n">&#x00F1;</string>
+    <!-- U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA -->
+    <string name="more_keys_for_c">&#x00E7;</string>
+    <string name="more_keys_for_g">g\'</string>
+</resources>
diff --git a/tools/maketext/res/values-tl/donottranslate-more-keys.xml b/tools/maketext/res/values-tl/donottranslate-more-keys.xml
new file mode 100644
index 0000000..383d55c
--- /dev/null
+++ b/tools/maketext/res/values-tl/donottranslate-more-keys.xml
@@ -0,0 +1,70 @@
+<?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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+         U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+         U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+         U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+         U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+         U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+         U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+         U+00E6: "æ" LATIN SMALL LETTER AE
+         U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+         U+00AA: "ª" FEMININE ORDINAL INDICATOR -->
+    <string name="more_keys_for_a">&#x00E1;,&#x00E0;,&#x00E4;,&#x00E2;,&#x00E3;,&#x00E5;,&#x0105;,&#x00E6;,&#x0101;,&#x00AA;</string>
+    <!-- U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+         U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+         U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+         U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+         U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+         U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+         U+0113: "ē" LATIN SMALL LETTER E WITH MACRON -->
+    <string name="more_keys_for_e">&#x00E9;,&#x00E8;,&#x00EB;,&#x00EA;,&#x0119;,&#x0117;,&#x0113;</string>
+    <!-- U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+         U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+         U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+         U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+         U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+         U+012B: "ī" LATIN SMALL LETTER I WITH MACRON -->
+    <string name="more_keys_for_i">&#x00ED;,&#x00EF;,&#x00EC;,&#x00EE;,&#x012F;,&#x012B;</string>
+    <!-- U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+         U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+         U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+         U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+         U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+         U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+         U+0153: "œ" LATIN SMALL LIGATURE OE
+         U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+         U+00BA: "º" MASCULINE ORDINAL INDICATOR -->
+    <string name="more_keys_for_o">&#x00F3;,&#x00F2;,&#x00F6;,&#x00F4;,&#x00F5;,&#x00F8;,&#x0153;,&#x014D;,&#x00BA;</string>
+    <!-- U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+         U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+         U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+         U+016B: "ū" LATIN SMALL LETTER U WITH MACRON -->
+    <string name="more_keys_for_u">&#x00FA;,&#x00FC;,&#x00F9;,&#x00FB;,&#x016B;</string>
+    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+         U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE -->
+    <string name="more_keys_for_n">&#x00F1;,&#x0144;</string>
+    <!-- U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+         U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+         U+010D: "č" LATIN SMALL LETTER C WITH CARON -->
+    <string name="more_keys_for_c">&#x00E7;,&#x0107;,&#x010D;</string>
+</resources>
diff --git a/tools/maketext/res/values-uk/donottranslate-more-keys.xml b/tools/maketext/res/values-uk/donottranslate-more-keys.xml
index 3239704..6d4b5f9 100644
--- a/tools/maketext/res/values-uk/donottranslate-more-keys.xml
+++ b/tools/maketext/res/values-uk/donottranslate-more-keys.xml
@@ -20,12 +20,16 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- U+0449: "щ" CYRILLIC SMALL LETTER SHCHA -->
     <string name="keylabel_for_east_slavic_row1_9">&#x0449;</string>
+    <!-- U+0457: "ї" CYRILLIC SMALL LETTER YI -->
+    <string name="keylabel_for_east_slavic_row1_12">&#x0457;</string>
     <!-- U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I -->
     <string name="keylabel_for_east_slavic_row2_1">&#x0456;</string>
+    <!-- U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE -->
+    <string name="keylabel_for_east_slavic_row2_11">&#x0454;</string>
     <!-- U+0438: "и" CYRILLIC SMALL LETTER I -->
     <string name="keylabel_for_east_slavic_row3_5">&#x0438;</string>
-    <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
-    <string name="more_keys_for_cyrillic_ha">&#x044A;</string>
+    <!-- U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN -->
+    <string name="more_keys_for_cyrillic_ghe">&#x0491;</string>
     <!-- U+0457: "ї" CYRILLIC SMALL LETTER YI -->
     <string name="more_keys_for_east_slavic_row2_1">&#x0457;</string>
     <!-- U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN -->
diff --git a/tools/maketext/res/values-zu/donottranslate-more-keys.xml b/tools/maketext/res/values-zu/donottranslate-more-keys.xml
new file mode 100644
index 0000000..1917915
--- /dev/null
+++ b/tools/maketext/res/values-zu/donottranslate-more-keys.xml
@@ -0,0 +1,64 @@
+<?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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- This is the same as English -->
+    <!-- U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+         U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+         U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+         U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+         U+00E6: "æ" LATIN SMALL LETTER AE
+         U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+         U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+         U+0101: "ā" LATIN SMALL LETTER A WITH MACRON -->
+    <string name="more_keys_for_a">&#x00E0;,&#x00E1;,&#x00E2;,&#x00E4;,&#x00E6;,&#x00E3;,&#x00E5;,&#x0101;</string>
+    <!-- U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+         U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+         U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+         U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+         U+0113: "ē" LATIN SMALL LETTER E WITH MACRON -->
+    <string name="more_keys_for_e">&#x00E8;,&#x00E9;,&#x00EA;,&#x00EB;,&#x0113;</string>
+    <!-- U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+         U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+         U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+         U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+         U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE -->
+    <string name="more_keys_for_i">&#x00EE;,&#x00EF;,&#x00ED;,&#x012B;,&#x00EC;</string>
+    <!-- U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+         U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+         U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+         U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+         U+0153: "œ" LATIN SMALL LIGATURE OE
+         U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+         U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+         U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE -->
+    <string name="more_keys_for_o">&#x00F4;,&#x00F6;,&#x00F2;,&#x00F3;,&#x0153;,&#x00F8;,&#x014D;,&#x00F5;</string>
+    <!-- U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+         U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+         U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+         U+016B: "ū" LATIN SMALL LETTER U WITH MACRON -->
+    <string name="more_keys_for_u">&#x00FB;,&#x00FC;,&#x00F9;,&#x00FA;,&#x016B;</string>
+    <!-- U+00DF: "ß" LATIN SMALL LETTER SHARP S -->
+    <string name="more_keys_for_s">&#x00DF;</string>
+    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE -->
+    <string name="more_keys_for_n">&#x00F1;</string>
+    <!-- U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA -->
+    <string name="more_keys_for_c">&#x00E7;</string>
+</resources>
diff --git a/tools/maketext/res/values/donottranslate-more-keys.xml b/tools/maketext/res/values/donottranslate-more-keys.xml
index 922b42d..4d7100b 100644
--- a/tools/maketext/res/values/donottranslate-more-keys.xml
+++ b/tools/maketext/res/values/donottranslate-more-keys.xml
@@ -44,12 +44,13 @@
     <string name="more_keys_for_nordic_row2_10"></string>
     <string name="more_keys_for_nordic_row2_11"></string>
     <string name="keylabel_for_east_slavic_row1_9"></string>
+    <string name="keylabel_for_east_slavic_row1_12"></string>
     <string name="keylabel_for_east_slavic_row2_1"></string>
+    <string name="keylabel_for_east_slavic_row2_11"></string>
     <string name="keylabel_for_east_slavic_row3_5"></string>
     <string name="more_keys_for_cyrillic_u"></string>
-    <string name="more_keys_for_cyrillic_ye"></string>
     <string name="more_keys_for_cyrillic_en"></string>
-    <string name="more_keys_for_cyrillic_ha"></string>
+    <string name="more_keys_for_cyrillic_ghe"></string>
     <string name="more_keys_for_east_slavic_row2_1"></string>
     <string name="more_keys_for_cyrillic_o"></string>
     <string name="more_keys_for_cyrillic_soft_sign"></string>
@@ -158,7 +159,6 @@
     <string name="more_keys_for_symbols_0">&#x207F;,&#x2205;</string>
     <string name="keylabel_for_comma">,</string>
     <string name="more_keys_for_comma"></string>
-    <string name="keylabel_for_symbols_exclamation">!</string>
     <string name="keylabel_for_symbols_question">\?</string>
     <string name="keylabel_for_symbols_semicolon">;</string>
     <string name="keylabel_for_symbols_percent">%</string>
@@ -177,6 +177,14 @@
     <string name="keylabel_for_apostrophe">\'</string>
     <string name="keyhintlabel_for_apostrophe">\"</string>
     <string name="more_keys_for_apostrophe">\"</string>
+    <string name="more_keys_for_q"></string>
+    <string name="more_keys_for_x"></string>
+    <string name="keylabel_for_q">q</string>
+    <string name="keylabel_for_w">w</string>
+    <string name="keylabel_for_y">y</string>
+    <string name="keylabel_for_x">x</string>
+    <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE -->
+    <string name="keylabel_for_spanish_row2_10">&#x00F1;</string>
     <string name="more_keys_for_am_pm">!fixedColumnOrder!2,!hasLabels!,\@string/label_time_am,\@string/label_time_pm</string>
     <string name="settings_as_more_key">!icon/settings_key|!code/key_settings</string>
     <string name="shortcut_as_more_key">!icon/shortcut_key|!code/key_shortcut</string>
diff --git a/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java b/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java
index 366d73e..6d6bc0e 100644
--- a/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java
+++ b/tools/maketext/src/com/android/inputmethod/latin/maketext/JarUtils.java
@@ -26,15 +26,14 @@
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 
-public class JarUtils {
-    private static final String MANIFEST = "META-INF/MANIFEST.MF";
-
+public final class JarUtils {
     private JarUtils() {
         // This utility class is not publicly instantiable.
     }
 
-    public static JarFile getJarFile(final ClassLoader loader) {
-        final URL resUrl = loader.getResource(MANIFEST);
+    public static JarFile getJarFile(final Class<?> mainClass) {
+        final String mainClassPath = "/" + mainClass.getName().replace('.', '/') + ".class";
+        final URL resUrl = mainClass.getResource(mainClassPath);
         if (!resUrl.getProtocol().equals("jar")) {
             throw new RuntimeException("Should run as jar");
         }
diff --git a/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java b/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java
index a5abcf1..4a92369 100644
--- a/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java
+++ b/tools/maketext/src/com/android/inputmethod/latin/maketext/LabelText.java
@@ -58,7 +58,7 @@
 
     public static void main(final String[] args) {
         final Options options = new Options(args);
-        final JarFile jar = JarUtils.getJarFile(LabelText.class.getClassLoader());
+        final JarFile jar = JarUtils.getJarFile(LabelText.class);
         final MoreKeysResources resources = new MoreKeysResources(jar);
         resources.writeToJava(options.mJava);
     }
diff --git a/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java b/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java
index a483593..37ac0d0 100644
--- a/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java
+++ b/tools/maketext/src/com/android/inputmethod/latin/maketext/MoreKeysResources.java
@@ -100,7 +100,7 @@
                 final File outputFile = new File(outPackage,
                         JAVA_TEMPLATE.replace(".tmpl", ".java"));
                 outPackage.mkdirs();
-                ps = new PrintStream(outputFile);
+                ps = new PrintStream(outputFile, "UTF-8");
             }
             lnr = new LineNumberReader(new InputStreamReader(JarUtils.openResource(template)));
             inflateTemplate(lnr, ps);