merge in jb-ub-latinimegoogle-azuki history after reset to jb-ub-latinimegoogle
diff --git a/dictionaries/en_GB_wordlist.combined.gz b/dictionaries/en_GB_wordlist.combined.gz
index ce70150..b3e5cfc 100644
--- a/dictionaries/en_GB_wordlist.combined.gz
+++ b/dictionaries/en_GB_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/en_US_wordlist.combined.gz b/dictionaries/en_US_wordlist.combined.gz
index e14c33b..67328d8 100644
--- a/dictionaries/en_US_wordlist.combined.gz
+++ b/dictionaries/en_US_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/en_wordlist.combined.gz b/dictionaries/en_wordlist.combined.gz
index 5276aaa..7fc6cff 100644
--- a/dictionaries/en_wordlist.combined.gz
+++ b/dictionaries/en_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/fr_wordlist.combined.gz b/dictionaries/fr_wordlist.combined.gz
index ccc31c8..c7c6977 100644
--- a/dictionaries/fr_wordlist.combined.gz
+++ b/dictionaries/fr_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/nb_wordlist.combined.gz b/dictionaries/nb_wordlist.combined.gz
index d0d3d8b..f663bbe 100644
--- a/dictionaries/nb_wordlist.combined.gz
+++ b/dictionaries/nb_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/ru_wordlist.combined.gz b/dictionaries/ru_wordlist.combined.gz
index 9f4d937..4f92805 100644
--- a/dictionaries/ru_wordlist.combined.gz
+++ b/dictionaries/ru_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/sv_wordlist.combined.gz b/dictionaries/sv_wordlist.combined.gz
index b10c14b..c107ca9 100644
--- a/dictionaries/sv_wordlist.combined.gz
+++ b/dictionaries/sv_wordlist.combined.gz
Binary files differ
diff --git a/java/res/raw/main_en.dict b/java/res/raw/main_en.dict
index e2fd258..526761c 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 c2941b7..7520898 100644
--- a/java/res/raw/main_fr.dict
+++ b/java/res/raw/main_fr.dict
Binary files differ
diff --git a/java/res/raw/main_ru.dict b/java/res/raw/main_ru.dict
index 9f3884d..216ff09 100644
--- a/java/res/raw/main_ru.dict
+++ b/java/res/raw/main_ru.dict
Binary files differ
diff --git a/java/res/values-af/dictionary-pack.xml b/java/res/values-af/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-af/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-am/dictionary-pack.xml b/java/res/values-am/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-am/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ar/dictionary-pack.xml b/java/res/values-ar/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ar/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-be/dictionary-pack.xml b/java/res/values-be/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-be/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-bg/dictionary-pack.xml b/java/res/values-bg/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-bg/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ca/dictionary-pack.xml b/java/res/values-ca/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ca/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-cs/dictionary-pack.xml b/java/res/values-cs/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-cs/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-da/dictionary-pack.xml b/java/res/values-da/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-da/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-de/dictionary-pack.xml b/java/res/values-de/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-de/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-el/dictionary-pack.xml b/java/res/values-el/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-el/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-en-rGB/dictionary-pack.xml b/java/res/values-en-rGB/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-en-rGB/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-es-rUS/dictionary-pack.xml b/java/res/values-es-rUS/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-es-rUS/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-es/dictionary-pack.xml b/java/res/values-es/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-es/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-et/dictionary-pack.xml b/java/res/values-et/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-et/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-fa/dictionary-pack.xml b/java/res/values-fa/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-fa/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-fi/dictionary-pack.xml b/java/res/values-fi/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-fi/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-fr/dictionary-pack.xml b/java/res/values-fr/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-fr/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-hi/dictionary-pack.xml b/java/res/values-hi/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-hi/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-hr/dictionary-pack.xml b/java/res/values-hr/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-hr/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-hu/dictionary-pack.xml b/java/res/values-hu/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-hu/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-in/dictionary-pack.xml b/java/res/values-in/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-in/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-is/dictionary-pack.xml b/java/res/values-is/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-is/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-it/dictionary-pack.xml b/java/res/values-it/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-it/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-iw/dictionary-pack.xml b/java/res/values-iw/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-iw/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ja/dictionary-pack.xml b/java/res/values-ja/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ja/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ka/dictionary-pack.xml b/java/res/values-ka/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ka/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ko/dictionary-pack.xml b/java/res/values-ko/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ko/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-lt/dictionary-pack.xml b/java/res/values-lt/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-lt/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-lv/dictionary-pack.xml b/java/res/values-lv/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-lv/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-mk/dictionary-pack.xml b/java/res/values-mk/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-mk/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-mn/dictionary-pack.xml b/java/res/values-mn/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-mn/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ms/dictionary-pack.xml b/java/res/values-ms/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ms/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-nb/dictionary-pack.xml b/java/res/values-nb/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-nb/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-nl/dictionary-pack.xml b/java/res/values-nl/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-nl/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-pl/dictionary-pack.xml b/java/res/values-pl/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-pl/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-pt-rPT/dictionary-pack.xml b/java/res/values-pt-rPT/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-pt-rPT/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-pt/dictionary-pack.xml b/java/res/values-pt/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-pt/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-rm/dictionary-pack.xml b/java/res/values-rm/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-rm/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ro/dictionary-pack.xml b/java/res/values-ro/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ro/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-ru/dictionary-pack.xml b/java/res/values-ru/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-ru/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-sk/dictionary-pack.xml b/java/res/values-sk/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-sk/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-sl/dictionary-pack.xml b/java/res/values-sl/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-sl/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-sr/dictionary-pack.xml b/java/res/values-sr/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-sr/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-sv/dictionary-pack.xml b/java/res/values-sv/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-sv/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-sw/dictionary-pack.xml b/java/res/values-sw/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-sw/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-th/dictionary-pack.xml b/java/res/values-th/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-th/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-tl/dictionary-pack.xml b/java/res/values-tl/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-tl/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-tr/dictionary-pack.xml b/java/res/values-tr/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-tr/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-uk/dictionary-pack.xml b/java/res/values-uk/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-uk/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-vi/dictionary-pack.xml b/java/res/values-vi/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-vi/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-zh-rCN/dictionary-pack.xml b/java/res/values-zh-rCN/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-zh-rCN/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-zh-rTW/dictionary-pack.xml b/java/res/values-zh-rTW/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-zh-rTW/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values-zu/dictionary-pack.xml b/java/res/values-zu/dictionary-pack.xml
new file mode 100644
index 0000000..f65d45b
--- /dev/null
+++ b/java/res/values-zu/dictionary-pack.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for dictionary_pack_settings_activity (664691545147898274) -->
+    <skip />
+    <!-- no translation found for authority (8773166495153016489) -->
+    <skip />
+    <string name="default_metadata_uri" msgid="6889596349847015153"></string>
+    <!-- no translation found for local_metadata_filename (4634356913689271331) -->
+    <skip />
+</resources>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 3a7b39e..a71e7cc 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -85,6 +85,8 @@
         <attr name="slidingKeyInputEnable" format="boolean" />
         <attr name="slidingKeyInputPreviewColor" format="color" />
         <attr name="slidingKeyInputPreviewWidth" format="dimension" />
+        <attr name="slidingKeyInputPreviewBodyRatio" format="integer" />
+        <attr name="slidingKeyInputPreviewShadowRatio" format="integer" />
         <!-- Key repeat start timeout -->
         <attr name="keyRepeatStartTimeout" format="integer" />
         <!-- Key repeat interval in millisecond. -->
@@ -115,6 +117,8 @@
         <attr name="gesturePreviewTrailColor" format="color" />
         <attr name="gesturePreviewTrailStartWidth" format="dimension" />
         <attr name="gesturePreviewTrailEndWidth" format="dimension" />
+        <attr name="gesturePreviewTrailBodyRatio" format="integer" />
+        <attr name="gesturePreviewTrailShadowRatio" format="integer" />
         <!-- Delay after gesture input and gesture floating preview text dismissing in millisecond -->
         <attr name="gestureFloatingPreviewTextLingerTimeout" format="integer" />
         <!-- Attributes for GestureFloatingPreviewText -->
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index a90ba80..d4fff62 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -58,6 +58,9 @@
     <bool name="config_sliding_key_input_enabled">true</bool>
     <!-- Sliding key input preview parameters -->
     <dimen name="config_sliding_key_input_preview_width">8.0dp</dimen>
+    <!-- Percentages of sliding key input preview body and shadow, in proportion to the width. -->
+    <integer name="config_sliding_key_input_preview_body_ratio">80</integer>
+    <integer name="config_sliding_key_input_preview_shadow_ratio">50</integer>
     <integer name="config_key_repeat_start_timeout">400</integer>
     <integer name="config_key_repeat_interval">50</integer>
     <integer name="config_default_longpress_key_timeout">300</integer>  <!-- milliseconds -->
diff --git a/java/res/values/dictionary-pack.xml b/java/res/values/dictionary-pack.xml
index 4109bcb..3fdc671 100644
--- a/java/res/values/dictionary-pack.xml
+++ b/java/res/values/dictionary-pack.xml
@@ -20,4 +20,8 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="dictionary_pack_client_id" translatable="false">com.android.inputmethod.latin</string>
     <string name="dictionary_pack_metadata_uri" translatable="false"></string>
+    <string name="dictionary_pack_settings_activity">com.android.inputmethod.dictionarypack.DictionarySettingsActivity</string>
+    <string name="authority">com.android.inputmethod.dictionarypack.aosp</string>
+    <string name="default_metadata_uri"></string>
+    <string name="local_metadata_filename">metadata.json</string>
 </resources>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index dd42acf..db33ad8 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -103,6 +103,9 @@
     <!-- Gesture preview trail parameters -->
     <dimen name="gesture_preview_trail_start_width">10.0dp</dimen>
     <dimen name="gesture_preview_trail_end_width">2.5dp</dimen>
+    <!-- Percentages of gesture preview taril body and shadow, in proportion to the trail width. -->
+    <integer name="gesture_preview_trail_body_ratio">80</integer>
+    <integer name="gesture_preview_trail_shadow_ratio">50</integer>
     <!-- Gesture floating preview text parameters -->
     <dimen name="gesture_floating_preview_text_size">24dp</dimen>
     <dimen name="gesture_floating_preview_text_offset">73dp</dimen>
diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml
index 1e70fbb..d2554ee 100644
--- a/java/res/values/donottranslate.xml
+++ b/java/res/values/donottranslate.xml
@@ -211,11 +211,4 @@
     </string-array>
 
     <string name="settings_warning_researcher_mode">Attention!  You are using the special keyboard for research purposes.</string>
-
-    <!-- dictionary pack settings -->
-    <string name="dictionary_pack_settings_activity">com.android.inputmethod.dictionarypack.DictionarySettingsActivity</string>
-    <string name="authority">com.android.inputmethod.dictionarypack.aosp</string>
-    <string name="default_metadata_uri"></string>
-    <string name="local_metadata_filename">metadata.json</string>
-
 </resources>
diff --git a/java/res/values/keypress-vibration-durations.xml b/java/res/values/keypress-vibration-durations.xml
index 9b1d543..10400be 100644
--- a/java/res/values/keypress-vibration-durations.xml
+++ b/java/res/values/keypress-vibration-durations.xml
@@ -18,13 +18,17 @@
 */
 -->
 <resources>
+    <!-- Build.HARDWARE,duration_in_milliseconds -->
     <string-array name="keypress_vibration_durations" translatable="false">
-        <!-- Build.HARDWARE,duration_in_milliseconds -->
+        <!-- Nexus S -->
         <item>herring,5</item>
+        <!-- Galaxy Nexus -->
         <item>tuna,5</item>
+        <!-- Nexus 4 -->
         <item>mako,5</item>
+        <!-- Nexus 10 -->
         <item>manta,16</item>
         <!-- Default value for unknown device -->
-        <item>DEFAULT,10</item>
+        <item>DEFAULT,20</item>
     </string-array>
 </resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 201fc70..f5e2441 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -422,11 +422,11 @@
 
     <!-- Title of an option for usability study mode -->
     <string name="prefs_usability_study_mode">Usability study mode</string>
-    <!-- Title of the settings for key long press delay [CHAR LIMIT=30] -->
+    <!-- Title of the settings for key long press delay [CHAR LIMIT=35] -->
     <string name="prefs_key_longpress_timeout_settings">Key long press delay</string>
-    <!-- Title of the settings for keypress vibration duration [CHAR LIMIT=30] -->
+    <!-- Title of the settings for keypress vibration duration [CHAR LIMIT=35] -->
     <string name="prefs_keypress_vibration_duration_settings">Keypress vibration duration</string>
-    <!-- Title of the settings for keypress sound volume [CHAR LIMIT=30] -->
+    <!-- Title of the settings for keypress sound volume [CHAR LIMIT=35] -->
     <string name="prefs_keypress_sound_volume_settings">Keypress sound volume</string>
     <!-- Title of the settings for reading an external dictionary file -->
     <string name="prefs_read_external_dictionary">Read external dictionary file</string>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index fb59c74..436e080 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -70,6 +70,8 @@
         <item name="gesturePreviewTrailColor">@color/highlight_color_default</item>
         <item name="gesturePreviewTrailStartWidth">@dimen/gesture_preview_trail_start_width</item>
         <item name="gesturePreviewTrailEndWidth">@dimen/gesture_preview_trail_end_width</item>
+        <item name="gesturePreviewTrailBodyRatio">@integer/gesture_preview_trail_body_ratio</item>
+        <item name="gesturePreviewTrailShadowRatio">@integer/gesture_preview_trail_shadow_ratio</item>
         <!-- Common attributes of MainKeyboardView -->
         <item name="keyHysteresisDistance">@dimen/config_key_hysteresis_distance</item>
         <item name="keyHysteresisDistanceForSlidingModifier">@dimen/config_key_hysteresis_distance_for_sliding_modifier</item>
@@ -78,6 +80,8 @@
         <item name="slidingKeyInputEnable">@bool/config_sliding_key_input_enabled</item>
         <item name="slidingKeyInputPreviewColor">@color/highlight_translucent_color_default</item>
         <item name="slidingKeyInputPreviewWidth">@dimen/config_sliding_key_input_preview_width</item>
+        <item name="slidingKeyInputPreviewBodyRatio">@integer/config_sliding_key_input_preview_body_ratio</item>
+        <item name="slidingKeyInputPreviewShadowRatio">@integer/config_sliding_key_input_preview_shadow_ratio</item>
         <item name="keyRepeatStartTimeout">@integer/config_key_repeat_start_timeout</item>
         <item name="keyRepeatInterval">@integer/config_key_repeat_interval</item>
         <item name="longPressShiftLockTimeout">@integer/config_longpress_shift_lock_timeout</item>
diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml
index 7839462..1581e5f 100644
--- a/java/res/xml/prefs.xml
+++ b/java/res/xml/prefs.xml
@@ -90,14 +90,17 @@
             android:summary="@string/gesture_input_summary"
             android:persistent="true"
             android:defaultValue="true" />
+        <!-- TODO: Move these two options to the advanced settings. -->
         <CheckBoxPreference
             android:key="pref_gesture_floating_preview_text"
+            android:dependency="gesture_input"
             android:title="@string/gesture_floating_preview_text"
             android:summary="@string/gesture_floating_preview_text_summary"
             android:persistent="true"
             android:defaultValue="true" />
         <CheckBoxPreference
             android:key="pref_gesture_preview_trail"
+            android:dependency="gesture_input"
             android:title="@string/gesture_preview_trail"
             android:persistent="true"
             android:defaultValue="true" />
@@ -139,10 +142,6 @@
                 android:summary="@string/include_other_imes_in_language_switch_list_summary"
                 android:persistent="true"
                 android:defaultValue="false" />
-            <PreferenceScreen
-                android:fragment="com.android.inputmethod.latin.AdditionalSubtypeSettings"
-                android:key="custom_input_styles"
-                android:title="@string/custom_input_styles_title" />
             <!-- Values for popup dismiss delay are added programmatically -->
             <CheckBoxPreference
                 android:key="pref_sliding_key_input_preview"
@@ -150,6 +149,10 @@
                 android:summary="@string/sliding_key_input_preview_summary"
                 android:persistent="true"
                 android:defaultValue="true" />
+            <PreferenceScreen
+                android:fragment="com.android.inputmethod.latin.AdditionalSubtypeSettings"
+                android:key="custom_input_styles"
+                android:title="@string/custom_input_styles_title" />
             <ListPreference
                 android:key="pref_key_preview_popup_dismiss_delay"
                 android:title="@string/key_preview_popup_dismiss_delay" />
diff --git a/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java
new file mode 100644
index 0000000..ff6561c
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.compat;
+
+import android.content.Context;
+import android.provider.UserDictionary.Words;
+
+import java.lang.reflect.Method;
+import java.util.Locale;
+
+public final class UserDictionaryCompatUtils {
+    // UserDictionary.Words#addWord(Context, String, int, String, Locale) was introduced
+    // in API level 16 (Build.VERSION_CODES.JELLY_BEAN).
+    private static final Method METHOD_addWord = CompatUtils.getMethod(Words.class, "addWord",
+            Context.class, String.class, Integer.TYPE, String.class, Locale.class);
+
+    public static void addWord(final Context context, final String word, final int freq,
+            final String shortcut, final Locale locale) {
+        if (hasNewerAddWord()) {
+            CompatUtils.invoke(Words.class, null, METHOD_addWord, context, word, freq, shortcut,
+                    locale);
+        } else {
+            // Fall back to the pre-JellyBean method.
+            final int localeType;
+            if (null == locale) {
+                localeType = Words.LOCALE_TYPE_ALL;
+            } else {
+                localeType = Words.LOCALE_TYPE_CURRENT;
+            }
+            Words.addWord(context, word, freq, localeType);
+        }
+    }
+
+    private static final boolean hasNewerAddWord() {
+        return null != METHOD_addWord;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index d15f14f..4e41b77 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -207,7 +207,8 @@
      * Update keyboard shift state triggered by connected EditText status change.
      */
     public void updateShiftState() {
-        mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState());
+        mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(),
+                mLatinIME.getCurrentRecapitalizeState());
     }
 
     // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
@@ -276,7 +277,8 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void requestUpdatingShiftState() {
-        mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState());
+        mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(),
+                mLatinIME.getCurrentRecapitalizeState());
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 43d28be..e4e75c3 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -26,10 +26,8 @@
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.Region;
-import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.util.SparseArray;
 import android.view.View;
 
 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
@@ -73,15 +71,15 @@
  */
 public class KeyboardView extends View {
     // XML attributes
-    protected final KeyVisualAttributes mKeyVisualAttributes;
+    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 Drawable mKeyBackground;
-    protected final Rect mKeyBackgroundPadding = new Rect();
+    private final float mVerticalCorrection;
+    private final Drawable mKeyBackground;
+    private final Rect mKeyBackgroundPadding = new Rect();
 
     // HORIZONTAL ELLIPSIS "...", character for popup hint.
     private static final String POPUP_HINT_CHAR = "\u2026";
@@ -113,10 +111,6 @@
     private final Canvas mOffscreenCanvas = new Canvas();
     private final Paint mPaint = new Paint();
     private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
-    // This sparse array caches key label text height in pixel indexed by key label text size.
-    private static final SparseArray<Float> sTextHeightCache = 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' };
 
@@ -134,15 +128,15 @@
         mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset(
                 R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
         mKeyHintLetterPadding = keyboardViewAttr.getDimension(
-                R.styleable.KeyboardView_keyHintLetterPadding, 0);
+                R.styleable.KeyboardView_keyHintLetterPadding, 0.0f);
         mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension(
-                R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
+                R.styleable.KeyboardView_keyPopupHintLetterPadding, 0.0f);
         mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
-                R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
+                R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0.0f);
         mKeyTextShadowRadius = keyboardViewAttr.getFloat(
                 R.styleable.KeyboardView_keyTextShadowRadius, 0.0f);
         mVerticalCorrection = keyboardViewAttr.getDimension(
-                R.styleable.KeyboardView_verticalCorrection, 0);
+                R.styleable.KeyboardView_verticalCorrection, 0.0f);
         keyboardViewAttr.recycle();
 
         final TypedArray keyAttr = context.obtainStyledAttributes(attrs,
@@ -185,6 +179,14 @@
         return mKeyboard;
     }
 
+    protected float getVerticalCorrection() {
+        return mVerticalCorrection;
+    }
+
+    protected void updateKeyDrawParams(final int keyHeight) {
+        mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
+    }
+
     @Override
     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
         if (mKeyboard != null) {
@@ -213,7 +215,7 @@
             }
             onDrawKeyboard(mOffscreenCanvas);
         }
-        canvas.drawBitmap(mOffscreenBuffer, 0, 0, null);
+        canvas.drawBitmap(mOffscreenBuffer, 0.0f, 0.0f, null);
     }
 
     private boolean maybeAllocateOffscreenBuffer() {
@@ -333,7 +335,7 @@
         canvas.translate(bgX, bgY);
         background.draw(canvas);
         if (LatinImeLogger.sVISUALDEBUG) {
-            drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint());
+            drawRectangle(canvas, 0.0f, 0.0f, bgWidth, bgHeight, 0x80c00000, new Paint());
         }
         canvas.translate(-bgX, -bgY);
     }
@@ -347,7 +349,7 @@
         final float centerY = keyHeight * 0.5f;
 
         if (LatinImeLogger.sVISUALDEBUG) {
-            drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint());
+            drawRectangle(canvas, 0.0f, 0.0f, keyWidth, keyHeight, 0x800000c0, new Paint());
         }
 
         // Draw key label.
@@ -357,14 +359,16 @@
             final String label = key.mLabel;
             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);
+            final float labelCharHeight = TypefaceUtils.getCharHeight(
+                    KEY_LABEL_REFERENCE_CHAR, paint);
+            final float labelCharWidth = TypefaceUtils.getCharWidth(
+                    KEY_LABEL_REFERENCE_CHAR, paint);
 
             // Vertical label text alignment.
-            final float baseline = centerY + labelCharHeight / 2;
+            final float baseline = centerY + labelCharHeight / 2.0f;
 
             // Horizontal label text alignment
-            float labelWidth = 0;
+            float labelWidth = 0.0f;
             if (key.isAlignLeft()) {
                 positionX = mKeyLabelHorizontalPadding;
                 paint.setTextAlign(Align.LEFT);
@@ -373,31 +377,31 @@
                 paint.setTextAlign(Align.RIGHT);
             } else if (key.isAlignLeftOfCenter()) {
                 // TODO: Parameterise this?
-                positionX = centerX - labelCharWidth * 7 / 4;
+                positionX = centerX - labelCharWidth * 7.0f / 4.0f;
                 paint.setTextAlign(Align.LEFT);
             } else if (key.hasLabelWithIconLeft() && icon != null) {
-                labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
+                labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth()
                         + LABEL_ICON_MARGIN * keyWidth;
-                positionX = centerX + labelWidth / 2;
+                positionX = centerX + labelWidth / 2.0f;
                 paint.setTextAlign(Align.RIGHT);
             } else if (key.hasLabelWithIconRight() && icon != null) {
-                labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
+                labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth()
                         + LABEL_ICON_MARGIN * keyWidth;
-                positionX = centerX - labelWidth / 2;
+                positionX = centerX - labelWidth / 2.0f;
                 paint.setTextAlign(Align.LEFT);
             } else {
                 positionX = centerX;
                 paint.setTextAlign(Align.CENTER);
             }
             if (key.needsXScale()) {
-                paint.setTextScaleX(
-                        Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint)));
+                paint.setTextScaleX(Math.min(1.0f,
+                        (keyWidth * MAX_LABEL_RATIO) / TypefaceUtils.getLabelWidth(label, paint)));
             }
 
             paint.setColor(key.selectTextColor(params));
             if (key.isEnabled()) {
                 // Set a drop shadow for the text
-                paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor);
+                paint.setShadowLayer(mKeyTextShadowRadius, 0.0f, 0.0f, params.mTextShadowColor);
             } else {
                 // Make label invisible
                 paint.setColor(Color.TRANSPARENT);
@@ -405,7 +409,7 @@
             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);
+            paint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.TRANSPARENT);
             paint.setTextScaleX(1.0f);
 
             if (icon != null) {
@@ -413,10 +417,10 @@
                 final int iconHeight = icon.getIntrinsicHeight();
                 final int iconY = (keyHeight - iconHeight) / 2;
                 if (key.hasLabelWithIconLeft()) {
-                    final int iconX = (int)(centerX - labelWidth / 2);
+                    final int iconX = (int)(centerX - labelWidth / 2.0f);
                     drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
                 } else if (key.hasLabelWithIconRight()) {
-                    final int iconX = (int)(centerX + labelWidth / 2 - iconWidth);
+                    final int iconX = (int)(centerX + labelWidth / 2.0f - iconWidth);
                     drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
                 }
             }
@@ -439,20 +443,23 @@
                 // The hint label is placed just right of the key label. Used mainly on
                 // "phone number" layout.
                 // TODO: Generalize the following calculations.
-                hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2;
-                hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
+                hintX = positionX
+                        + TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2.0f;
+                hintY = centerY
+                        + TypefaceUtils.getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f;
                 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 - mKeyShiftedLetterHintPadding
-                        - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
+                        - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f;
                 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 - mKeyHintLetterPadding
-                        - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2;
+                        - TypefaceUtils.getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint)
+                        / 2.0f;
                 hintY = -paint.ascent();
                 paint.setTextAlign(Align.CENTER);
             }
@@ -506,7 +513,7 @@
         paint.setColor(params.mHintLabelColor);
         paint.setTextAlign(Align.CENTER);
         final float hintX = keyWidth - mKeyHintLetterPadding
-                - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
+                - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f;
         final float hintY = keyHeight - mKeyPopupHintLetterPadding;
         canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
 
@@ -517,54 +524,6 @@
         }
     }
 
-    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;
-        if (face == Typeface.DEFAULT) {
-            return codePointOffset + labelSize;
-        } else if (face == Typeface.DEFAULT_BOLD) {
-            return codePointOffset + labelSize + 0x1000;
-        } else if (face == Typeface.MONOSPACE) {
-            return codePointOffset + labelSize + 0x2000;
-        } else {
-            return codePointOffset + labelSize;
-        }
-    }
-
-    // Working variable for the following methods.
-    private final Rect mTextBounds = new Rect();
-
-    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;
-
-        paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
-        final float height = mTextBounds.height();
-        sTextHeightCache.put(key, height);
-        return height;
-    }
-
-    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;
-
-        paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
-        final float width = mTextBounds.width();
-        sTextWidthCache.put(key, width);
-        return width;
-    }
-
-    // TODO: Remove this method.
-    public float getLabelWidth(final String label, final Paint paint) {
-        paint.getTextBounds(label, 0, label.length(), mTextBounds);
-        return mTextBounds.width();
-    }
-
     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);
@@ -578,7 +537,7 @@
         paint.setStyle(Paint.Style.STROKE);
         paint.setStrokeWidth(1.0f);
         paint.setColor(color);
-        canvas.drawLine(0, y, w, y, paint);
+        canvas.drawLine(0.0f, y, w, y, paint);
     }
 
     private static void drawVerticalLine(final Canvas canvas, final float x, final float h,
@@ -586,7 +545,7 @@
         paint.setStyle(Paint.Style.STROKE);
         paint.setStrokeWidth(1.0f);
         paint.setColor(color);
-        canvas.drawLine(x, 0, x, h, paint);
+        canvas.drawLine(x, 0.0f, x, h, paint);
     }
 
     private static void drawRectangle(final Canvas canvas, final float x, final float y,
@@ -595,15 +554,20 @@
         paint.setStrokeWidth(1.0f);
         paint.setColor(color);
         canvas.translate(x, y);
-        canvas.drawRect(0, 0, w, h, paint);
+        canvas.drawRect(0.0f, 0.0f, w, h, paint);
         canvas.translate(-x, -y);
     }
 
-    public Paint newDefaultLabelPaint() {
+    public Paint newLabelPaint(final Key key) {
         final Paint paint = new Paint();
         paint.setAntiAlias(true);
-        paint.setTypeface(mKeyDrawParams.mTypeface);
-        paint.setTextSize(mKeyDrawParams.mLabelSize);
+        if (key == null) {
+            paint.setTypeface(mKeyDrawParams.mTypeface);
+            paint.setTextSize(mKeyDrawParams.mLabelSize);
+        } else {
+            paint.setTypeface(key.selectTypeface(mKeyDrawParams));
+            paint.setTextSize(key.selectTextSize(mKeyDrawParams));
+        }
         return paint;
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 745e7df..ba78d01 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -530,9 +530,9 @@
                 R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
 
         final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension(
-                R.styleable.MainKeyboardView_keyHysteresisDistance, 0);
+                R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f);
         final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension(
-                R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0);
+                R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f);
         mKeyDetector = new KeyDetector(
                 keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier);
         mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr);
@@ -655,7 +655,7 @@
         mKeyTimerHandler.cancelLongPressTimer();
         super.setKeyboard(keyboard);
         mKeyDetector.setKeyboard(
-                keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
+                keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
         PointerTracker.setKeyDetector(mKeyDetector);
         mTouchScreenRegulator.setKeyboardGeometry(keyboard.mOccupiedWidth);
         mMoreKeysKeyboardCache.clear();
@@ -1329,7 +1329,7 @@
 
         // Overlay a dark rectangle to dim.
         if (mNeedsToDimEntireKeyboard) {
-            canvas.drawRect(0, 0, getWidth(), getHeight(), mBackgroundDimAlphaPaint);
+            canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mBackgroundDimAlphaPaint);
         }
     }
 
@@ -1353,9 +1353,10 @@
         }
     }
 
-    private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) {
+    private static boolean fitsTextIntoWidth(final int width, final String text,
+            final Paint paint) {
         paint.setTextScaleX(1.0f);
-        final float textWidth = getLabelWidth(text, paint);
+        final float textWidth = TypefaceUtils.getLabelWidth(text, paint);
         if (textWidth < width) {
             return true;
         }
@@ -1366,12 +1367,12 @@
         }
 
         paint.setTextScaleX(scaleX);
-        return getLabelWidth(text, paint) < width;
+        return TypefaceUtils.getLabelWidth(text, paint) < width;
     }
 
     // Layout language name on spacebar.
-    private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype,
-            final int width) {
+    private static String layoutLanguageOnSpacebar(final Paint paint,
+            final InputMethodSubtype subtype, final int width) {
         // Choose appropriate language name to fit into the width.
         final String fullText = getFullDisplayName(subtype);
         if (fitsTextIntoWidth(width, fullText, paint)) {
@@ -1460,7 +1461,7 @@
             return "";
         }
         final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
-        return StringUtils.toTitleCase(locale.getLanguage(), locale);
+        return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale);
     }
 
     // Get InputMethodSubtype's middle display name in its locale.
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
index 66c3014..ae08a59 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.keyboard;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
 
@@ -73,10 +74,11 @@
                 final int rowHeight, final int coordXInParent, final int parentKeyboardWidth,
                 final boolean isFixedColumnOrder, final int dividerWidth) {
             mIsFixedOrder = isFixedColumnOrder;
-            if (parentKeyboardWidth / keyWidth < maxColumns) {
+            if (parentKeyboardWidth / keyWidth < Math.min(numKeys, maxColumns)) {
                 throw new IllegalArgumentException(
                         "Keyboard is too small to hold more keys keyboard: "
-                                + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
+                                + parentKeyboardWidth + " " + keyWidth + " "
+                                + numKeys + " " + maxColumns);
             }
             mDefaultKeyWidth = keyWidth;
             mDefaultRowHeight = rowHeight;
@@ -257,7 +259,6 @@
         private static final float LABEL_PADDING_RATIO = 0.2f;
         private static final float DIVIDER_RATIO = 0.2f;
 
-
         /**
          * The builder of MoreKeysKeyboard.
          * @param context the context of {@link MoreKeysKeyboardView}.
@@ -289,11 +290,23 @@
                 // be considered because the vertical positions of both backgrounds were already
                 // adjusted with their bottom paddings deducted.
                 width = keyPreviewDrawParams.mPreviewVisibleWidth;
-                height = keyPreviewDrawParams.mPreviewVisibleHeight
-                        + mParams.mVerticalGap;
+                height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap;
+                // TODO: Remove this check.
+                if (width == 0) {
+                    throw new IllegalArgumentException(
+                            "Zero width key detected: " + parentKey + " in " + parentKeyboard.mId);
+                }
             } else {
-                width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth);
+                width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth,
+                        context.getResources());
                 height = parentKeyboard.mMostCommonKeyHeight;
+                // TODO: Remove this check.
+                if (width == 0) {
+                    throw new IllegalArgumentException(
+                            "Zero width calculated: " + parentKey
+                            + " moreKeys=" + java.util.Arrays.toString(parentKey.mMoreKeys)
+                            + " in " + parentKeyboard.mId);
+                }
             }
             final int dividerWidth;
             if (parentKey.needsDividersInMoreKeys()) {
@@ -310,22 +323,18 @@
         }
 
         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.setTypeface(parentKey.selectTypeface(view.mKeyDrawParams));
-            paint.setTextSize(parentKey.selectMoreKeyTextSize(view.mKeyDrawParams));
+                final int minKeyWidth, final Resources res) {
+            final float padding =
+                    res.getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding)
+                    + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0.0f);
+            final Paint paint = view.newLabelPaint(parentKey);
             int maxWidth = minKeyWidth;
             for (final MoreKeySpec spec : parentKey.mMoreKeys) {
                 final String label = spec.mLabel;
                 // If the label is single letter, minKeyWidth is enough to hold the label.
                 if (label != null && StringUtils.codePointCount(label) > 1) {
-                    final int width = (int)view.getLabelWidth(label, paint) + padding;
-                    if (maxWidth < width) {
-                        maxWidth = width;
-                    }
+                    maxWidth = Math.max(maxWidth,
+                            (int)(TypefaceUtils.getLabelWidth(label, paint) + padding));
                 }
             }
             return maxWidth;
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index 0d42ab2..a82fb79 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -33,7 +33,7 @@
 public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel {
     private final int[] mCoordinates = CoordinateUtils.newInstance();
 
-    private final KeyDetector mKeyDetector;
+    protected final KeyDetector mKeyDetector;
     private Controller mController;
     protected KeyboardActionListener mListener;
     private int mOriginX;
@@ -71,7 +71,7 @@
     public void setKeyboard(final Keyboard keyboard) {
         super.setKeyboard(keyboard);
         mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
-                -getPaddingTop() + mVerticalCorrection);
+                -getPaddingTop() + getVerticalCorrection());
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index dcfae73..91c4319 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -441,10 +441,10 @@
 
     // Returns true if keyboard has been changed by this callback.
     private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
-        if (sInGesture) {
+        if (sInGesture || mIsDetectingGesture) {
             return false;
         }
-        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
+        final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
         if (DEBUG_LISTENER) {
             Log.d(TAG, String.format("[%d] onPress    : %s%s%s", mPointerId,
                     KeyDetector.printableCode(key),
@@ -468,7 +468,7 @@
     // primaryCode is different from {@link Key#mCode}.
     private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
             final int y, final long eventTime) {
-        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
+        final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
         final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
         final int code = altersCode ? key.getAltCode() : primaryCode;
         if (DEBUG_LISTENER) {
@@ -500,10 +500,10 @@
     // primaryCode is different from {@link Key#mCode}.
     private void callListenerOnRelease(final Key key, final int primaryCode,
             final boolean withSliding) {
-        if (sInGesture) {
+        if (sInGesture || mIsDetectingGesture) {
             return;
         }
-        final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
+        final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
         if (DEBUG_LISTENER) {
             Log.d(TAG, String.format("[%d] onRelease  : %s%s%s%s", mPointerId,
                     Constants.printableCode(primaryCode),
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 32cee73..b77e378 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -26,7 +26,7 @@
 
 import java.util.Arrays;
 
-public final class ProximityInfo {
+public class ProximityInfo {
     private static final String TAG = ProximityInfo.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -79,22 +79,21 @@
         mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
     }
 
-    private static ProximityInfo createDummyProximityInfo() {
-        return new ProximityInfo("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
-    }
-
-    public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximityCharsArray,
-            final int rowSize, final int gridWidth, final int gridHeight) {
-        final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo();
-        spellCheckerProximityInfo.mNativeProximityInfo =
-                spellCheckerProximityInfo.setProximityInfoNative("" /* locale */,
-                        gridWidth /* displayWidth */, gridHeight /* displayHeight */,
-                        gridWidth, gridHeight, 1 /* mostCommonKeyWidth */, proximityCharsArray,
-                        0 /* keyCount */, null /*keyXCoordinates */, null /* keyYCoordinates */,
-                        null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */,
-                        null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */,
-                        null /* sweetSpotRadii */);
-        return spellCheckerProximityInfo;
+    /**
+     * Constructor for subclasses such as
+     * {@link com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo}.
+     */
+    protected ProximityInfo(final int[] proximityCharsArray, final int gridWidth,
+            final int gridHeight) {
+        this("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
+        mNativeProximityInfo = setProximityInfoNative("" /* locale */,
+                gridWidth /* displayWidth */, gridHeight /* displayHeight */,
+                gridWidth, gridHeight, 1 /* mostCommonKeyWidth */,
+                1 /* mostCommonKeyHeight */, proximityCharsArray, 0 /* keyCount */,
+                null /*keyXCoordinates */, null /* keyYCoordinates */,
+                null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */,
+                null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */,
+                null /* sweetSpotRadii */);
     }
 
     private long mNativeProximityInfo;
@@ -105,9 +104,10 @@
     // TODO: Stop passing proximityCharsArray
     private static native long setProximityInfoNative(String locale,
             int displayWidth, int displayHeight, int gridWidth, int gridHeight,
-            int mostCommonKeyWidth, int[] proximityCharsArray, int keyCount, int[] keyXCoordinates,
-            int[] keyYCoordinates, int[] keyWidths, int[] keyHeights, int[] keyCharCodes,
-            float[] sweetSpotCenterXs, float[] sweetSpotCenterYs, float[] sweetSpotRadii);
+            int mostCommonKeyWidth, int mostCommonKeyHeight, int[] proximityCharsArray,
+            int keyCount, int[] keyXCoordinates, int[] keyYCoordinates, int[] keyWidths,
+            int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterXs,
+            float[] sweetSpotCenterYs, float[] sweetSpotRadii);
 
     private static native void releaseProximityInfoNative(long nativeProximityInfo);
 
@@ -234,9 +234,9 @@
 
         // TODO: Stop passing proximityCharsArray
         return setProximityInfoNative(mLocaleStr, mKeyboardMinWidth, mKeyboardHeight,
-                mGridWidth, mGridHeight, mMostCommonKeyWidth, proximityCharsArray, keyCount,
-                keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
-                sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
+                mGridWidth, mGridHeight, mMostCommonKeyWidth, mMostCommonKeyHeight,
+                proximityCharsArray, keyCount, keyXCoordinates, keyYCoordinates, keyWidths,
+                keyHeights, keyCharCodes, sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
     }
 
     public long getNativeProximityInfo() {
diff --git a/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java b/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java
new file mode 100644
index 0000000..6a54e11
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard;
+
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.util.SparseArray;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+public final class TypefaceUtils {
+    private TypefaceUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    // This sparse array caches key label text height in pixel indexed by key label text size.
+    private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray();
+    // Working variable for the following method.
+    private static final Rect sTextHeightBounds = new Rect();
+
+    public static float getCharHeight(final char[] referenceChar, final Paint paint) {
+        final int key = getCharGeometryCacheKey(referenceChar[0], paint);
+        synchronized (sTextHeightCache) {
+            final Float cachedValue = sTextHeightCache.get(key);
+            if (cachedValue != null) {
+                return cachedValue;
+            }
+
+            paint.getTextBounds(referenceChar, 0, 1, sTextHeightBounds);
+            final float height = sTextHeightBounds.height();
+            sTextHeightCache.put(key, height);
+            return height;
+        }
+    }
+
+    // This sparse array caches key label text width in pixel indexed by key label text size.
+    private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray();
+    // Working variable for the following method.
+    private static final Rect sTextWidthBounds = new Rect();
+
+    public static float getCharWidth(final char[] referenceChar, final Paint paint) {
+        final int key = getCharGeometryCacheKey(referenceChar[0], paint);
+        synchronized (sTextWidthCache) {
+            final Float cachedValue = sTextWidthCache.get(key);
+            if (cachedValue != null) {
+                return cachedValue;
+            }
+
+            paint.getTextBounds(referenceChar, 0, 1, sTextWidthBounds);
+            final float width = sTextWidthBounds.width();
+            sTextWidthCache.put(key, width);
+            return width;
+        }
+    }
+
+    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;
+        if (face == Typeface.DEFAULT) {
+            return codePointOffset + labelSize;
+        } else if (face == Typeface.DEFAULT_BOLD) {
+            return codePointOffset + labelSize + 0x1000;
+        } else if (face == Typeface.MONOSPACE) {
+            return codePointOffset + labelSize + 0x2000;
+        } else {
+            return codePointOffset + labelSize;
+        }
+    }
+
+    public static float getLabelWidth(final String label, final Paint paint) {
+        final Rect textBounds = new Rect();
+        paint.getTextBounds(label, 0, label.length(), textBounds);
+        return textBounds.width();
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
index e3e6d39..0c05061 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
@@ -37,6 +37,7 @@
 final class GesturePreviewTrail {
     private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
 
+    // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}.
     private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
@@ -50,6 +51,9 @@
         public final int mTrailColor;
         public final float mTrailStartWidth;
         public final float mTrailEndWidth;
+        public final float mTrailBodyRatio;
+        public boolean mTrailShadowEnabled;
+        public final float mTrailShadowRatio;
         public final int mFadeoutStartDelay;
         public final int mFadeoutDuration;
         public final int mUpdateInterval;
@@ -63,6 +67,14 @@
                     R.styleable.MainKeyboardView_gesturePreviewTrailStartWidth, 0.0f);
             mTrailEndWidth = mainKeyboardViewAttr.getDimension(
                     R.styleable.MainKeyboardView_gesturePreviewTrailEndWidth, 0.0f);
+            final int PERCENTAGE_INT = 100;
+            mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_gesturePreviewTrailBodyRatio, PERCENTAGE_INT)
+                    / (float)PERCENTAGE_INT;
+            final int trailShadowRatioInt = mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_gesturePreviewTrailShadowRatio, 0);
+            mTrailShadowEnabled = (trailShadowRatioInt > 0);
+            mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT;
             mFadeoutStartDelay = mainKeyboardViewAttr.getInt(
                     R.styleable.MainKeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
             mFadeoutDuration = mainKeyboardViewAttr.getInt(
@@ -90,7 +102,13 @@
     }
 
     public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) {
-        final int trailSize = mEventTimes.getLength();
+        synchronized (mEventTimes) {
+            addStrokeLocked(stroke, downTime);
+        }
+    }
+
+    private void addStrokeLocked(final GestureStrokeWithPreviewPoints stroke, final long downTime) {
+            final int trailSize = mEventTimes.getLength();
         stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
         if (mEventTimes.getLength() == trailSize) {
             return;
@@ -169,6 +187,13 @@
      */
     public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
             final Rect outBoundsRect, final Params params) {
+        synchronized (mEventTimes) {
+            return drawGestureTrailLocked(canvas, paint, outBoundsRect, params);
+        }
+    }
+
+    private boolean drawGestureTrailLocked(final Canvas canvas, final Paint paint,
+            final Rect outBoundsRect, final Params params) {
         // Initialize bounds rectangle.
         outBoundsRect.setEmpty();
         final int trailSize = mEventTimes.getLength();
@@ -205,14 +230,22 @@
                 final float r2 = getWidth(elapsedTime, params) / 2.0f;
                 // Draw trail line only when the current point isn't a down point.
                 if (!isDownEventXCoord(xCoords[i])) {
-                    final Path path = roundedLine.makePath(p1x, p1y, r1, p2x, p2y, r2);
+                    final float body1 = r1 * params.mTrailBodyRatio;
+                    final float body2 = r2 * params.mTrailBodyRatio;
+                    final Path path = roundedLine.makePath(p1x, p1y, body1, p2x, p2y, body2);
                     if (path != null) {
+                        roundedLine.getBounds(mRoundedLineBounds);
+                        if (params.mTrailShadowEnabled) {
+                            final float shadow2 = r2 * params.mTrailShadowRatio;
+                            paint.setShadowLayer(shadow2, 0.0f, 0.0f, params.mTrailColor);
+                            final int shadowInset = -(int)Math.ceil(shadow2);
+                            mRoundedLineBounds.inset(shadowInset, shadowInset);
+                        }
+                        // Take union for the bounds.
+                        outBoundsRect.union(mRoundedLineBounds);
                         final int alpha = getAlpha(elapsedTime, params);
                         paint.setAlpha(alpha);
                         canvas.drawPath(path, paint);
-                        // Take union for the bounds.
-                        roundedLine.getBounds(mRoundedLineBounds);
-                        outBoundsRect.union(mRoundedLineBounds);
                     }
                 }
                 p1x = p2x;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 95d9ccb..b1d4997 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -20,6 +20,7 @@
 import android.util.Log;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.RecapitalizeStatus;
 
 /**
  * Keyboard state machine.
@@ -29,7 +30,7 @@
  * The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()},
  * {@link #onPressKey(int, boolean, int)}, {@link #onReleaseKey(int, boolean)},
  * {@link #onCodeInput(int, boolean, int)}, {@link #onCancelInput(boolean)},
- * {@link #onUpdateShiftState(int)}, {@link #onLongPressTimeout(int)}.
+ * {@link #onUpdateShiftState(int, int)}, {@link #onLongPressTimeout(int)}.
  *
  * The actions are {@link SwitchActions}'s methods.
  */
@@ -48,7 +49,7 @@
         public void setSymbolsShiftedKeyboard();
 
         /**
-         * Request to call back {@link KeyboardState#onUpdateShiftState(int)}.
+         * Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}.
          */
         public void requestUpdatingShiftState();
 
@@ -80,6 +81,7 @@
     private boolean mIsSymbolShifted;
     private boolean mPrevMainKeyboardWasShiftLocked;
     private boolean mPrevSymbolsKeyboardWasShifted;
+    private int mRecapitalizeMode;
 
     // For handling long press.
     private boolean mLongPressShiftLockFired;
@@ -110,6 +112,7 @@
 
     public KeyboardState(final SwitchActions switchActions) {
         mSwitchActions = switchActions;
+        mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
     }
 
     public void onLoadKeyboard() {
@@ -283,6 +286,7 @@
         mSwitchActions.setAlphabetKeyboard();
         mIsAlphabetMode = true;
         mIsSymbolShifted = false;
+        mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
         mSwitchState = SWITCH_STATE_ALPHA;
         mSwitchActions.requestUpdatingShiftState();
     }
@@ -386,11 +390,13 @@
         }
     }
 
-    public void onUpdateShiftState(final int autoCaps) {
+    public void onUpdateShiftState(final int autoCaps, final int recapitalizeMode) {
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + " " + this);
+            Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + ", recapitalizeMode="
+                    + recapitalizeMode + " " + this);
         }
-        updateAlphabetShiftState(autoCaps);
+        mRecapitalizeMode = recapitalizeMode;
+        updateAlphabetShiftState(autoCaps, recapitalizeMode);
     }
 
     // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
@@ -402,8 +408,28 @@
         resetKeyboardStateToAlphabet();
     }
 
-    private void updateAlphabetShiftState(final int autoCaps) {
+    private void updateShiftStateForRecapitalize(final int recapitalizeMode) {
+        switch (recapitalizeMode) {
+        case RecapitalizeStatus.CAPS_MODE_ALL_UPPER:
+            setShifted(SHIFT_LOCK_SHIFTED);
+            break;
+        case RecapitalizeStatus.CAPS_MODE_FIRST_WORD_UPPER:
+            setShifted(AUTOMATIC_SHIFT);
+            break;
+        case RecapitalizeStatus.CAPS_MODE_ALL_LOWER:
+        case RecapitalizeStatus.CAPS_MODE_ORIGINAL_MIXED_CASE:
+        default:
+            setShifted(UNSHIFT);
+        }
+    }
+
+    private void updateAlphabetShiftState(final int autoCaps, final int recapitalizeMode) {
         if (!mIsAlphabetMode) return;
+        if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != recapitalizeMode) {
+            // We are recapitalizing. Match the keyboard to the current recapitalize state.
+            updateShiftStateForRecapitalize(recapitalizeMode);
+            return;
+        }
         if (!mShiftKeyState.isReleasing()) {
             // Ignore update shift state event while the shift key is being pressed (including
             // chording).
@@ -421,6 +447,9 @@
 
     private void onPressShift() {
         mLongPressShiftLockFired = false;
+        // If we are recapitalizing, we don't do any of the normal processing, including
+        // importantly the double tap timer.
+        if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) return;
         if (mIsAlphabetMode) {
             mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapTimeout();
             if (!mIsInDoubleTapShiftKey) {
@@ -467,7 +496,11 @@
     }
 
     private void onReleaseShift(final boolean withSliding) {
-        if (mIsAlphabetMode) {
+        if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
+            // We are recapitalizing. We should match the keyboard state to the recapitalize
+            // state in priority.
+            updateShiftStateForRecapitalize(mRecapitalizeMode);
+        } else if (mIsAlphabetMode) {
             final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked();
             mIsInAlphabetUnshiftedFromShifted = false;
             if (mIsInDoubleTapShiftKey) {
@@ -597,7 +630,7 @@
 
         // If the code is a letter, update keyboard shift state.
         if (Constants.isLetterCode(code)) {
-            updateAlphabetShiftState(autoCaps);
+            updateAlphabetShiftState(autoCaps, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE);
         }
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index 2df7e5c..6bc6acc 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -34,175 +34,197 @@
     }
 
     private static final int INITIAL_CAPACITY = 10;
+    // Note: {@link #mExpandableArrayOfActivePointers} and {@link #mArraySize} are synchronized by
+    // {@link #mExpandableArrayOfActivePointers}
     private final ArrayList<Element> mExpandableArrayOfActivePointers =
             CollectionUtils.newArrayList(INITIAL_CAPACITY);
     private int mArraySize = 0;
 
-    public synchronized int size() {
-        return mArraySize;
-    }
-
-    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);
+    public int size() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            return mArraySize;
         }
-        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) {
+    public void add(final Element pointer) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            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 void remove(final Element pointer) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            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) {
-                    Log.w(TAG, "Found duplicated element in remove: " + pointer);
+                    // Shift this element toward the beginning of the expandableArray.
+                    expandableArray.set(newSize, element);
                 }
-                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: " + pointer + " " + this);
-        }
-        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 (!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;
         }
-        mArraySize = newSize;
+    }
+
+    public Element getOldestElement() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0);
+        }
+    }
+
+    public void releaseAllPointersOlderThan(final Element pointer, final long eventTime) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            if (DEBUG) {
+                Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this);
+            }
+            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 (!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(final long eventTime) {
         releaseAllPointersExcept(null, eventTime);
     }
 
-    public synchronized void releaseAllPointersExcept(final Element pointer,
-            final long eventTime) {
-        if (DEBUG) {
-            if (pointer == null) {
-                Log.d(TAG, "releaseAllPoniters: " + this);
-            } else {
-                Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this);
-            }
-        }
-        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);
+    public void releaseAllPointersExcept(final Element pointer, final long eventTime) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            if (DEBUG) {
+                if (pointer == null) {
+                    Log.d(TAG, "releaseAllPoniters: " + this);
+                } else {
+                    Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this);
                 }
-                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);
+            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++;
             }
-            newSize++;
+            mArraySize = 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.
+    public boolean hasModifierKeyOlderThan(final Element pointer) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            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;
+                }
             }
-            if (element.isModifier()) {
-                return true;
-            }
+            return false;
         }
-        return false;
     }
 
-    public synchronized boolean isAnyInSlidingKeyInput() {
-        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;
+    public boolean isAnyInSlidingKeyInput() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            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;
+                }
             }
+            return false;
         }
-        return false;
     }
 
-    public synchronized void cancelAllPointerTracker() {
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        for (int index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            element.cancelTracking();
+    public void cancelAllPointerTracker() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            for (int index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                element.cancelTracking();
+            }
         }
     }
 
     @Override
-    public synchronized String toString() {
-        final StringBuilder sb = new StringBuilder();
-        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(element.toString());
+    public String toString() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final StringBuilder sb = new StringBuilder();
+            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(element.toString());
+            }
+            return "[" + sb.toString() + "]";
         }
-        return "[" + sb.toString() + "]";
     }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java
index 37f4e35..33dbbaf 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java
@@ -30,7 +30,7 @@
  * Draw rubber band preview graphics during sliding key input.
  */
 public final class SlidingKeyInputPreview extends AbstractDrawingPreview {
-    private final int mPreviewWidth;
+    private final float mPreviewBodyRadius;
 
     private boolean mShowSlidingKeyInputPreview;
     private final int[] mPreviewFrom = CoordinateUtils.newInstance();
@@ -44,8 +44,20 @@
         super(drawingView);
         final int previewColor = mainKeyboardViewAttr.getColor(
                 R.styleable.MainKeyboardView_slidingKeyInputPreviewColor, 0);
-        mPreviewWidth = mainKeyboardViewAttr.getDimensionPixelSize(
-                R.styleable.MainKeyboardView_slidingKeyInputPreviewWidth, 0);
+        final float previewRadius = mainKeyboardViewAttr.getDimension(
+                R.styleable.MainKeyboardView_slidingKeyInputPreviewWidth, 0) / 2.0f;
+        final int PERCENTAGE_INT = 100;
+        final float previewBodyRatio = (float)mainKeyboardViewAttr.getInt(
+                R.styleable.MainKeyboardView_slidingKeyInputPreviewBodyRatio, PERCENTAGE_INT)
+                / (float)PERCENTAGE_INT;
+        mPreviewBodyRadius = previewRadius * previewBodyRatio;
+        final int previewShadowRatioInt = mainKeyboardViewAttr.getInt(
+                R.styleable.MainKeyboardView_slidingKeyInputPreviewShadowRatio, 0);
+        if (previewShadowRatioInt > 0) {
+            final float previewShadowRatio = (float)previewShadowRatioInt / (float)PERCENTAGE_INT;
+            final float shadowRadius = previewRadius * previewShadowRatio;
+            mPaint.setShadowLayer(shadowRadius, 0.0f, 0.0f, previewColor);
+        }
         mPaint.setColor(previewColor);
     }
 
@@ -65,7 +77,7 @@
         }
 
         // TODO: Finalize the rubber band preview implementation.
-        final int radius = mPreviewWidth / 2;
+        final float radius = mPreviewBodyRadius;
         final Path path = mRoundedLine.makePath(
                 CoordinateUtils.x(mPreviewFrom), CoordinateUtils.y(mPreviewFrom), radius,
                 CoordinateUtils.x(mPreviewTo), CoordinateUtils.y(mPreviewTo), radius);
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 562e1d0..42f7136 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -422,7 +422,7 @@
 
     private static void reinitializeClientRecordInDictionaryContentProvider(final Context context,
             final ContentProviderClient client, final String clientId) throws RemoteException {
-        final String metadataFileUri = context.getString(R.string.dictionary_pack_metadata_uri);
+        final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context);
         if (TextUtils.isEmpty(metadataFileUri)) return;
         // Tell the content provider to reset all information about this client id
         final Uri metadataContentUri = getProviderUriBuilder(clientId)
diff --git a/java/src/com/android/inputmethod/latin/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/CapsModeUtils.java
index 1012cd5..4b8d1ac 100644
--- a/java/src/com/android/inputmethod/latin/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/CapsModeUtils.java
@@ -41,7 +41,7 @@
         if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) {
             return s.toUpperCase(locale);
         } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) {
-            return StringUtils.toTitleCase(s, locale);
+            return StringUtils.capitalizeFirstCodePoint(s, locale);
         } else {
             return s;
         }
diff --git a/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java b/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java
new file mode 100644
index 0000000..792a446
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.text.TextUtils;
+import android.view.inputmethod.CompletionInfo;
+
+import java.util.Arrays;
+
+/**
+ * Utilities to do various stuff with CompletionInfo.
+ */
+public class CompletionInfoUtils {
+    private CompletionInfoUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static CompletionInfo[] removeNulls(final CompletionInfo[] src) {
+        int j = 0;
+        final CompletionInfo[] dst = new CompletionInfo[src.length];
+        for (int i = 0; i < src.length; ++i) {
+            if (null != src[i] && !TextUtils.isEmpty(src[i].getText())) {
+                dst[j] = src[i];
+                ++j;
+            }
+        }
+        return Arrays.copyOfRange(dst, 0, j);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 50e5023..86bb255 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -160,6 +160,8 @@
     public static final int CODE_DOUBLE_QUOTE = '"';
     public static final int CODE_QUESTION_MARK = '?';
     public static final int CODE_EXCLAMATION_MARK = '!';
+    public static final int CODE_SLASH = '/';
+    public static final int CODE_COMMERCIAL_AT = '@';
     // 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?
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 8b5a76a..75c2cf2 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -173,7 +173,8 @@
                 // capitalization of i.
                 final int wordLen = StringUtils.codePointCount(word);
                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
-                    super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS);
+                    super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
+                            false /* isNotAWord */);
                     if (!TextUtils.isEmpty(prevWord)) {
                         if (mUseFirstLastBigrams) {
                             super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM);
@@ -251,7 +252,7 @@
     }
 
     private static boolean isValidName(final String name) {
-        if (name != null && -1 == name.indexOf('@')) {
+        if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) {
             return true;
         }
         return false;
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index ff3d83f..9691fa2 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -37,6 +37,8 @@
     public static final String TYPE_USER = "user";
     // User history dictionary internal to LatinIME.
     public static final String TYPE_USER_HISTORY = "history";
+    // Spawned by resuming suggestions. Comes from a span that was in the TextView.
+    public static final String TYPE_RESUMED = "resumed";
     protected final String mDictType;
 
     public Dictionary(final String dictType) {
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 97dc6a8..4b1975a 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -176,14 +176,15 @@
      */
     // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
     // considering performance regression.
-    protected void addWord(final String word, final String shortcutTarget, final int frequency) {
+    protected void addWord(final String word, final String shortcutTarget, final int frequency,
+            final boolean isNotAWord) {
         if (shortcutTarget == null) {
-            mFusionDictionary.add(word, frequency, null, false /* isNotAWord */);
+            mFusionDictionary.add(word, frequency, null, isNotAWord);
         } else {
             // TODO: Do this in the subclass, with this class taking an arraylist.
             final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
             shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
-            mFusionDictionary.add(word, frequency, shortcutTargets, false /* isNotAWord */);
+            mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord);
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
index ecb2014..46194f6 100644
--- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
@@ -33,7 +33,6 @@
     private static final int[] SUPPRESSING_AUTO_SPACES_FIELD_VARIATION = {
         InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
         InputType.TYPE_TEXT_VARIATION_PASSWORD,
-        InputType.TYPE_TEXT_VARIATION_URI,
         InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
         InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD };
     public static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1;
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 56b1c78..0e1c4dc 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -44,7 +44,9 @@
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.text.InputType;
+import android.text.SpannableString;
 import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
 import android.util.Log;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
@@ -72,6 +74,7 @@
 import com.android.inputmethod.keyboard.KeyboardId;
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.RichInputConnection.Range;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.Utils.Stats;
 import com.android.inputmethod.latin.define.ProductionFlag;
@@ -158,6 +161,7 @@
             mPositionalInfoForUserDictPendingAddition = null;
     private final WordComposer mWordComposer = new WordComposer();
     private final RichInputConnection mConnection = new RichInputConnection(this);
+    private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
 
     // 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;
@@ -197,6 +201,7 @@
         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 MSG_RESUME_SUGGESTIONS = 4;
 
         private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
 
@@ -234,6 +239,9 @@
                 latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj,
                         msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
                 break;
+            case MSG_RESUME_SUGGESTIONS:
+                latinIme.restartSuggestionsOnWordTouchedByCursor();
+                break;
             }
         }
 
@@ -241,6 +249,10 @@
             sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
         }
 
+        public void postResumeSuggestions() {
+            sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
+        }
+
         public void cancelUpdateSuggestionStrip() {
             removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
         }
@@ -730,6 +742,7 @@
         resetComposingState(true /* alsoResetLastComposedWord */);
         mDeleteCount = 0;
         mSpaceState = SPACE_STATE_NONE;
+        mRecapitalizeStatus.deactivate();
         mCurrentlyPressedHardwareKeys.clear();
 
         if (mSuggestionStripView != null) {
@@ -910,13 +923,13 @@
                 resetEntireInputState(newSelStart);
             }
 
+            // We moved the cursor. If we are touching a word, we need to resume suggestion.
+            mHandler.postResumeSuggestions();
+            // Reset the last recapitalization.
+            mRecapitalizeStatus.deactivate();
             mKeyboardSwitcher.updateShiftState();
         }
         mExpectingUpdateSelection = false;
-        // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
-        // here. It would probably be too expensive to call directly here but we may want to post a
-        // message to delay it. The point would be to unify behavior between backspace to the
-        // end of a word and manually put the pointer at the end of the word.
 
         // Make a note of the cursor position
         mLastSelectionStart = newSelStart;
@@ -983,7 +996,6 @@
             }
         }
         if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
-        mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
         if (applicationSpecifiedCompletions == null) {
             clearSuggestionStrip();
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -991,6 +1003,8 @@
             }
             return;
         }
+        mApplicationSpecifiedCompletions =
+                CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
 
         final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
                 SuggestedWords.getFromApplicationSpecifiedCompletions(
@@ -1166,6 +1180,15 @@
                 SPACE_STATE_PHANTOM == mSpaceState);
     }
 
+    public int getCurrentRecapitalizeState() {
+        if (!mRecapitalizeStatus.isActive()
+                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+            // Not recapitalizing at the moment
+            return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
+        }
+        return mRecapitalizeStatus.getCurrentMode();
+    }
+
     // Factor in auto-caps and manual caps and compute the current caps mode.
     private int getActualCapsMode() {
         final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
@@ -1243,10 +1266,6 @@
         } else {
             wordToEdit = word;
         }
-        mPositionalInfoForUserDictPendingAddition =
-                new PositionalInfoForUserDictPendingAddition(
-                        wordToEdit, mLastSelectionEnd, getCurrentInputEditorInfo(),
-                        mLastComposedWord.mCapitalizedMode);
         mUserDictionary.addWordToUserDictionary(wordToEdit);
     }
 
@@ -1380,8 +1399,18 @@
             LatinImeLogger.logOnDelete(x, y);
             break;
         case Constants.CODE_SHIFT:
+            // Note: calling back to the keyboard on Shift key is handled in onPressKey()
+            // and onReleaseKey().
+            final Keyboard currentKeyboard = switcher.getKeyboard();
+            if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
+                // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
+                // alphabetic shift and shift while in symbol layout.
+                handleRecapitalize();
+            }
+            break;
         case Constants.CODE_SWITCH_ALPHA_SYMBOL:
-            // Shift and symbol key is handled in onPressKey() and onReleaseKey().
+            // Note: calling back to the keyboard on symbol key is handled in onPressKey()
+            // and onReleaseKey().
             break;
         case Constants.CODE_SETTINGS:
             onSettingsKeyPressed();
@@ -1459,7 +1488,13 @@
                                 "", mWordComposer.getTypedWord(), " ", mWordComposer);
                     }
                 }
-                commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+                if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+                    // If we are in the middle of a recorrection, we need to commit the recorrection
+                    // first so that we can insert the character at the current cursor position.
+                    resetEntireInputState(mLastSelectionStart);
+                } else {
+                    commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+                }
             }
             final int keyX, keyY;
             final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
@@ -1515,8 +1550,12 @@
             }
             final int wordComposerSize = mWordComposer.size();
             // Since isComposingWord() is true, the size is at least 1.
-            final int lastChar = mWordComposer.getCodeAt(wordComposerSize - 1);
-            if (wordComposerSize <= 1) {
+            final int lastChar = mWordComposer.getCodeBeforeCursor();
+            if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+                // If we are in the middle of a recorrection, we need to commit the recorrection
+                // first so that we can insert the batch input at the current cursor position.
+                resetEntireInputState(mLastSelectionStart);
+            } else if (wordComposerSize <= 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
@@ -1540,7 +1579,8 @@
             }
         } else {
             final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
-            if (mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
+            if (Character.isLetter(codePointBeforeCursor)
+                    || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
                 mSpaceState = SPACE_STATE_PHANTOM;
             }
         }
@@ -1551,7 +1591,8 @@
     private static final class BatchInputUpdater implements Handler.Callback {
         private final Handler mHandler;
         private LatinIME mLatinIme;
-        private boolean mInBatchInput; // synchronized using "this".
+        private final Object mLock = new Object();
+        private boolean mInBatchInput; // synchronized using {@link #mLock}.
 
         private BatchInputUpdater() {
             final HandlerThread handlerThread = new HandlerThread(
@@ -1582,21 +1623,25 @@
         }
 
         // Run in the UI thread.
-        public synchronized void onStartBatchInput(final LatinIME latinIme) {
-            mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
-            mLatinIme = latinIme;
-            mInBatchInput = true;
+        public void onStartBatchInput(final LatinIME latinIme) {
+            synchronized (mLock) {
+                mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
+                mLatinIme = latinIme;
+                mInBatchInput = true;
+            }
         }
 
         // Run in the Handler thread.
-        private synchronized void updateBatchInput(final InputPointers batchPointers) {
-            if (!mInBatchInput) {
-                // Batch input has ended or canceled while the message was being delivered.
-                return;
+        private void updateBatchInput(final InputPointers batchPointers) {
+            synchronized (mLock) {
+                if (!mInBatchInput) {
+                    // Batch input has ended or canceled while the message was being delivered.
+                    return;
+                }
+                final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
+                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                        suggestedWords, false /* dismissGestureFloatingPreviewText */);
             }
-            final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
-            mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
-                    suggestedWords, false /* dismissGestureFloatingPreviewText */);
         }
 
         // Run in the UI thread.
@@ -1609,19 +1654,23 @@
                     .sendToTarget();
         }
 
-        public synchronized void onCancelBatchInput() {
-            mInBatchInput = false;
-            mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
-                    SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
+        public void onCancelBatchInput() {
+            synchronized (mLock) {
+                mInBatchInput = false;
+                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                        SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
+            }
         }
 
         // Run in the UI thread.
-        public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers) {
-            mInBatchInput = false;
-            final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
-            mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
-                    suggestedWords, true /* dismissGestureFloatingPreviewText */);
-            return suggestedWords;
+        public SuggestedWords onEndBatchInput(final InputPointers batchPointers) {
+            synchronized (mLock) {
+                mInBatchInput = false;
+                final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
+                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                        suggestedWords, true /* dismissGestureFloatingPreviewText */);
+                return suggestedWords;
+            }
         }
 
         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
@@ -1717,6 +1766,12 @@
         // during key repeat.
         mHandler.postUpdateShiftState();
 
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can remove the character at the current cursor position.
+            resetEntireInputState(mLastSelectionStart);
+            // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
+        }
         if (mWordComposer.isComposingWord()) {
             final int length = mWordComposer.size();
             if (length > 0) {
@@ -1727,7 +1782,9 @@
                         ResearchLogger.getInstance().uncommitCurrentLogUnit(
                                 word, false /* dumpCurrentLogUnit */);
                     }
+                    final String rejectedSuggestion = mWordComposer.getTypedWord();
                     mWordComposer.reset();
+                    mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
                 } else {
                     mWordComposer.deleteLast();
                 }
@@ -1848,6 +1905,12 @@
             promotePhantomSpace();
         }
 
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can insert the character at the current cursor position.
+            resetEntireInputState(mLastSelectionStart);
+            isComposingWord = false;
+        }
         // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
         // thread here.
@@ -1902,6 +1965,38 @@
         }
     }
 
+    private void handleRecapitalize() {
+        if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
+        // If we have a recapitalize in progress, use it; otherwise, create a new one.
+        if (!mRecapitalizeStatus.isActive()
+                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+            mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
+                    mConnection.getSelectedText(0 /* flags, 0 for no styles */).toString(),
+                    mSettings.getCurrentLocale(), mSettings.getWordSeparators());
+            // We trim leading and trailing whitespace.
+            mRecapitalizeStatus.trim();
+            // Trimming the object may have changed the length of the string, and we need to
+            // reposition the selection handles accordingly. As this result in an IPC call,
+            // only do it if it's actually necessary, in other words if the recapitalize status
+            // is not set at the same place as before.
+            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+                mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
+            }
+        }
+        mRecapitalizeStatus.rotate();
+        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
+        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+        mConnection.deleteSurroundingText(numCharsDeleted, 0);
+        mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
+        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
+        // Match the keyboard to the new state.
+        mKeyboardSwitcher.updateShiftState();
+    }
+
     // Returns true if we did an autocorrection, false otherwise.
     private boolean handleSeparator(final int primaryCode, final int x, final int y,
             final int spaceState) {
@@ -1909,7 +2004,11 @@
             ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
         }
         boolean didAutoCorrect = false;
-        // Handle separator
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can insert the separator at the current cursor position.
+            resetEntireInputState(mLastSelectionStart);
+        }
         if (mWordComposer.isComposingWord()) {
             if (mSettings.getCurrent().mCorrectionEnabled) {
                 // TODO: maybe cache Strings in an <String> sparse array or something
@@ -2321,6 +2420,73 @@
     }
 
     /**
+     * Check if the cursor is touching a word. If so, restart suggestions on this word, else
+     * do nothing.
+     */
+    private void restartSuggestionsOnWordTouchedByCursor() {
+        // If the cursor is not touching a word, or if there is a selection, return right away.
+        if (mLastSelectionStart != mLastSelectionEnd) return;
+        if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return;
+        final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
+                0 /* additionalPrecedingWordsCount */);
+        if (null == range) return; // Happens if we don't have an input connection at all
+        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+        final String typedWord = range.mWord.toString();
+        if (range.mWord instanceof SpannableString) {
+            final SpannableString spannableString = (SpannableString)range.mWord;
+            int i = 0;
+            for (Object object : spannableString.getSpans(0, spannableString.length(),
+                    SuggestionSpan.class)) {
+                SuggestionSpan span = (SuggestionSpan)object;
+                for (String s : span.getSuggestions()) {
+                    ++i;
+                    if (!TextUtils.equals(s, typedWord)) {
+                        suggestions.add(new SuggestedWordInfo(s,
+                                SuggestionStripView.MAX_SUGGESTIONS - i,
+                                SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED));
+                    }
+                }
+            }
+        }
+        mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
+        mWordComposer.setCursorPositionWithinWord(range.mCharsBefore);
+        mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore,
+                mLastSelectionEnd + range.mCharsAfter);
+        final SuggestedWords suggestedWords;
+        if (suggestions.isEmpty()) {
+            // We come here if there weren't any suggestion spans on this word. We will try to
+            // compute suggestions for it instead.
+            final SuggestedWords suggestedWordsIncludingTypedWord =
+                    getSuggestedWords(Suggest.SESSION_TYPING);
+            if (suggestedWordsIncludingTypedWord.size() > 1) {
+                // We were able to compute new suggestions for this word.
+                // Remove the typed word, since we don't want to display it in this case.
+                // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false.
+                suggestedWords =
+                        suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord();
+            } else {
+                // No saved suggestions, and we were unable to compute any good one either.
+                // Rather than displaying an empty suggestion strip, we'll display the original
+                // word alone in the middle.
+                // Since there is only one word, willAutoCorrect is false.
+                suggestedWords = suggestedWordsIncludingTypedWord;
+            }
+        } else {
+            // We found suggestion spans in the word. We'll create the SuggestedWords out of
+            // them, and make willAutoCorrect false.
+            suggestedWords = new SuggestedWords(suggestions,
+                    true /* typedWordValid */, false /* willAutoCorrect */,
+                    false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
+                    false /* isPrediction */);
+        }
+
+        // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
+        // We never want to auto-correct on a resumed suggestion. Please refer to the three
+        // places above where suggestedWords is affected.
+        showSuggestionStrip(suggestedWords, typedWord);
+    }
+
+    /**
      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
      * word, else do nothing.
      */
@@ -2328,17 +2494,18 @@
         final CharSequence word =
                 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
         if (null != word) {
-            restartSuggestionsOnWordBeforeCursor(word);
+            final String wordString = word.toString();
+            restartSuggestionsOnWordBeforeCursor(wordString);
             // TODO: Handle the case where the user manually moves the cursor and then backs up over
             // a separator.  In that case, the current log unit should not be uncommitted.
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                ResearchLogger.getInstance().uncommitCurrentLogUnit(word.toString(),
+                ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
                         true /* dumpCurrentLogUnit */);
             }
         }
     }
 
-    private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
+    private void restartSuggestionsOnWordBeforeCursor(final String word) {
         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
         final int length = word.length();
         mConnection.deleteSurroundingText(length, 0);
@@ -2392,7 +2559,8 @@
 
     // This essentially inserts a space, and that's it.
     public void promotePhantomSpace() {
-        if (mSettings.getCurrent().shouldInsertSpacesAutomatically()) {
+        if (mSettings.getCurrent().shouldInsertSpacesAutomatically()
+                && !mConnection.textBeforeCursorLooksLikeURL()) {
             sendKeyCodePoint(Constants.CODE_SPACE);
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                 ResearchLogger.latinIME_promotePhantomSpace();
diff --git a/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
new file mode 100644
index 0000000..e6dc6db
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.Context;
+
+/**
+ * Helper class to get the metadata URI.
+ */
+public class MetadataFileUriGetter {
+    public static String getMetadataUri(Context context) {
+        return context.getString(R.string.dictionary_pack_metadata_uri);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java
new file mode 100644
index 0000000..8a704ab
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.StringUtils;
+
+import java.util.Locale;
+
+/**
+ * The status of the current recapitalize process.
+ */
+public class RecapitalizeStatus {
+    public static final int NOT_A_RECAPITALIZE_MODE = -1;
+    public static final int CAPS_MODE_ORIGINAL_MIXED_CASE = 0;
+    public static final int CAPS_MODE_ALL_LOWER = 1;
+    public static final int CAPS_MODE_FIRST_WORD_UPPER = 2;
+    public static final int CAPS_MODE_ALL_UPPER = 3;
+    // When adding a new mode, don't forget to update the CAPS_MODE_LAST constant.
+    public static final int CAPS_MODE_LAST = CAPS_MODE_ALL_UPPER;
+
+    private static final int[] ROTATION_STYLE = {
+        CAPS_MODE_ORIGINAL_MIXED_CASE,
+        CAPS_MODE_ALL_LOWER,
+        CAPS_MODE_FIRST_WORD_UPPER,
+        CAPS_MODE_ALL_UPPER
+    };
+
+    private static final int getStringMode(final String string, final String separators) {
+        if (StringUtils.isIdenticalAfterUpcase(string)) {
+            return CAPS_MODE_ALL_UPPER;
+        } else if (StringUtils.isIdenticalAfterDowncase(string)) {
+            return CAPS_MODE_ALL_LOWER;
+        } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) {
+            return CAPS_MODE_FIRST_WORD_UPPER;
+        } else {
+            return CAPS_MODE_ORIGINAL_MIXED_CASE;
+        }
+    }
+
+    /**
+     * We store the location of the cursor and the string that was there before the recapitalize
+     * action was done, and the location of the cursor and the string that was there after.
+     */
+    private int mCursorStartBefore;
+    private String mStringBefore;
+    private int mCursorStartAfter;
+    private int mCursorEndAfter;
+    private int mRotationStyleCurrentIndex;
+    private boolean mSkipOriginalMixedCaseMode;
+    private Locale mLocale;
+    private String mSeparators;
+    private String mStringAfter;
+    private boolean mIsActive;
+
+    public RecapitalizeStatus() {
+        // By default, initialize with dummy values that won't match any real recapitalize.
+        initialize(-1, -1, "", Locale.getDefault(), "");
+        deactivate();
+    }
+
+    public void initialize(final int cursorStart, final int cursorEnd, final String string,
+            final Locale locale, final String separators) {
+        mCursorStartBefore = cursorStart;
+        mStringBefore = string;
+        mCursorStartAfter = cursorStart;
+        mCursorEndAfter = cursorEnd;
+        mStringAfter = string;
+        final int initialMode = getStringMode(mStringBefore, separators);
+        mLocale = locale;
+        mSeparators = separators;
+        if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) {
+            mRotationStyleCurrentIndex = 0;
+            mSkipOriginalMixedCaseMode = false;
+        } else {
+            // Find the current mode in the array.
+            int currentMode;
+            for (currentMode = ROTATION_STYLE.length - 1; currentMode > 0; --currentMode) {
+                if (ROTATION_STYLE[currentMode] == initialMode) {
+                    break;
+                }
+            }
+            mRotationStyleCurrentIndex = currentMode;
+            mSkipOriginalMixedCaseMode = true;
+        }
+        mIsActive = true;
+    }
+
+    public void deactivate() {
+        mIsActive = false;
+    }
+
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    public boolean isSetAt(final int cursorStart, final int cursorEnd) {
+        return cursorStart == mCursorStartAfter && cursorEnd == mCursorEndAfter;
+    }
+
+    /**
+     * Rotate through the different possible capitalization modes.
+     */
+    public void rotate() {
+        final String oldResult = mStringAfter;
+        int count = 0; // Protection against infinite loop.
+        do {
+            mRotationStyleCurrentIndex = (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
+            if (CAPS_MODE_ORIGINAL_MIXED_CASE == ROTATION_STYLE[mRotationStyleCurrentIndex]
+                    && mSkipOriginalMixedCaseMode) {
+                mRotationStyleCurrentIndex =
+                        (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
+            }
+            ++count;
+            switch (ROTATION_STYLE[mRotationStyleCurrentIndex]) {
+            case CAPS_MODE_ORIGINAL_MIXED_CASE:
+                mStringAfter = mStringBefore;
+                break;
+            case CAPS_MODE_ALL_LOWER:
+                mStringAfter = mStringBefore.toLowerCase(mLocale);
+                break;
+            case CAPS_MODE_FIRST_WORD_UPPER:
+                mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators,
+                        mLocale);
+                break;
+            case CAPS_MODE_ALL_UPPER:
+                mStringAfter = mStringBefore.toUpperCase(mLocale);
+                break;
+            default:
+                mStringAfter = mStringBefore;
+            }
+        } while (mStringAfter.equals(oldResult) && count < ROTATION_STYLE.length + 1);
+        mCursorEndAfter = mCursorStartAfter + mStringAfter.length();
+    }
+
+    /**
+     * Remove leading/trailing whitespace from the considered string.
+     */
+    public void trim() {
+        final int len = mStringBefore.length();
+        int nonWhitespaceStart = 0;
+        for (; nonWhitespaceStart < len;
+                nonWhitespaceStart = mStringBefore.offsetByCodePoints(nonWhitespaceStart, 1)) {
+            final int codePoint = mStringBefore.codePointAt(nonWhitespaceStart);
+            if (!Character.isWhitespace(codePoint)) break;
+        }
+        int nonWhitespaceEnd = len;
+        for (; nonWhitespaceEnd > 0;
+                nonWhitespaceEnd = mStringBefore.offsetByCodePoints(nonWhitespaceEnd, -1)) {
+            final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd);
+            if (!Character.isWhitespace(codePoint)) break;
+        }
+        if (0 != nonWhitespaceStart || len != nonWhitespaceEnd) {
+            mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd;
+            mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart;
+            mStringAfter = mStringBefore =
+                    mStringBefore.substring(nonWhitespaceStart, nonWhitespaceEnd);
+        }
+    }
+
+    public String getRecapitalizedString() {
+        return mStringAfter;
+    }
+
+    public int getNewCursorStart() {
+        return mCursorStartAfter;
+    }
+
+    public int getNewCursorEnd() {
+        return mCursorEndAfter;
+    }
+
+    public int getCurrentMode() {
+        return ROTATION_STYLE[mRotationStyleCurrentIndex];
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 16744d1..8ed7ab2 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin;
 
 import android.inputmethodservice.InputMethodService;
+import android.text.SpannableString;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -181,6 +182,11 @@
         }
     }
 
+    public CharSequence getSelectedText(final int flags) {
+        if (null == mIC) return null;
+        return mIC.getSelectedText(flags);
+    }
+
     /**
      * Gets the caps modes we should be in after this specific string.
      *
@@ -392,7 +398,9 @@
     public void commitCompletion(final CompletionInfo completionInfo) {
         if (DEBUG_BATCH_NESTING) checkBatchEdit();
         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
-        final CharSequence text = completionInfo.getText();
+        CharSequence text = completionInfo.getText();
+        // text should never be null, but just in case, it's better to insert nothing than to crash
+        if (null == text) text = "";
         mCommittedTextBeforeComposingText.append(text);
         mCurrentCursorPosition += text.length() - mComposingText.length();
         mComposingText.setLength(0);
@@ -442,9 +450,9 @@
         public final int mCharsAfter;
 
         /** The actual characters that make up a word */
-        public final String mWord;
+        public final CharSequence mWord;
 
-        public Range(int charsBefore, int charsAfter, String word) {
+        public Range(int charsBefore, int charsAfter, CharSequence word) {
             if (charsBefore < 0 || charsAfter < 0) {
                 throw new IndexOutOfBoundsException();
             }
@@ -498,7 +506,7 @@
      *   separator. For example, if the field contains "he|llo world", where |
      *   represents the cursor, then "hello " will be returned.
      */
-    public String getWordAtCursor(String separators) {
+    public CharSequence getWordAtCursor(String separators) {
         // getWordRangeAtCursor returns null if the connection is null
         Range r = getWordRangeAtCursor(separators, 0);
         return (r == null) ? null : r.mWord;
@@ -517,8 +525,10 @@
         if (mIC == null || sep == null) {
             return null;
         }
-        final CharSequence before = mIC.getTextBeforeCursor(1000, 0);
-        final CharSequence after = mIC.getTextAfterCursor(1000, 0);
+        final CharSequence before = mIC.getTextBeforeCursor(1000,
+                InputConnection.GET_TEXT_WITH_STYLES);
+        final CharSequence after = mIC.getTextAfterCursor(1000,
+                InputConnection.GET_TEXT_WITH_STYLES);
         if (before == null || after == null) {
             return null;
         }
@@ -560,8 +570,9 @@
             }
         }
 
-        final String word = before.toString().substring(startIndexInBefore, before.length())
-                + after.toString().substring(0, endIndexInAfter);
+        final SpannableString word = new SpannableString(TextUtils.concat(
+                before.subSequence(startIndexInBefore, before.length()),
+                after.subSequence(0, endIndexInAfter)));
         return new Range(before.length() - startIndexInBefore, endIndexInAfter, word);
     }
 
@@ -709,4 +720,15 @@
         // position and the expected position, then it must be a belated update.
         return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0;
     }
+
+    /**
+     * Looks at the text just before the cursor to find out if it looks like a URL.
+     *
+     * The weakest point here is, if we don't have enough text bufferized, we may fail to realize
+     * we are in URL situation, but other places in this class have the same limitation and it
+     * does not matter too much in the practice.
+     */
+    public boolean textBeforeCursorLooksLikeURL() {
+        return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 8fbe843..72e0870 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -134,6 +134,14 @@
         return mSettingsValues.mIsInternal;
     }
 
+    public String getWordSeparators() {
+        return mSettingsValues.mWordSeparators;
+    }
+
+    public Locale getCurrentLocale() {
+        return mCurrentLocale;
+    }
+
     // Accessed from the settings interface, hence public
     public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
             final Resources res) {
diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java
index 99b572e..37ac2e3 100644
--- a/java/src/com/android/inputmethod/latin/SettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java
@@ -25,7 +25,10 @@
     @Override
     public Intent getIntent() {
         final Intent intent = super.getIntent();
-        intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
+        final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
+        if (fragment == null) {
+            intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
+        }
         intent.putExtra(EXTRA_NO_HEADERS, true);
         return intent;
     }
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index 5405a5e..a96c997 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -16,7 +16,6 @@
 
 package com.android.inputmethod.latin;
 
-import android.app.Activity;
 import android.app.backup.BackupManager;
 import android.content.Context;
 import android.content.Intent;
@@ -115,12 +114,8 @@
             if (FeedbackUtils.isFeedbackFormSupported()) {
                 feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                     @Override
-                    public boolean onPreferenceClick(Preference arg0) {
-                        final Activity activity = getActivity();
-                        FeedbackUtils.showFeedbackForm(activity);
-                        if (!activity.isFinishing()) {
-                            activity.finish();
-                        }
+                    public boolean onPreferenceClick(final Preference pref) {
+                        FeedbackUtils.showFeedbackForm(getActivity());
                         return true;
                     }
                 });
@@ -232,10 +227,6 @@
         } else if (key.equals(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY)) {
             setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST,
                     Settings.readShowsLanguageSwitchKey(prefs));
-        } else if (key.equals(Settings.PREF_GESTURE_INPUT)) {
-            final boolean gestureInputEnabled = Settings.readGestureInputEnabled(prefs, res);
-            setPreferenceEnabled(Settings.PREF_GESTURE_PREVIEW_TRAIL, gestureInputEnabled);
-            setPreferenceEnabled(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, gestureInputEnabled);
         } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) {
             LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity());
         }
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index 59ad28f..d5ee58a 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -106,10 +106,19 @@
         }
     }
 
-    public static String toTitleCase(final String s, final Locale locale) {
+    public static String capitalizeFirstCodePoint(final String s, final Locale locale) {
         if (s.length() <= 1) {
-            // TODO: is this really correct? Shouldn't this be s.toUpperCase()?
-            return s;
+            return s.toUpperCase(locale);
+        }
+        // Please refer to the comment below in
+        // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings
+        final int cutoff = s.offsetByCodePoints(0, 1);
+        return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff);
+    }
+
+    public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) {
+        if (s.length() <= 1) {
+            return s.toUpperCase(locale);
         }
         // TODO: fix the bugs below
         // - This does not work for Greek, because it returns upper case instead of title case.
@@ -213,4 +222,129 @@
         if (1 == capsCount) return CAPITALIZE_FIRST;
         return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
     }
+
+    public static boolean isIdenticalAfterUpcase(final String text) {
+        final int len = text.length();
+        for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+            final int codePoint = text.codePointAt(i);
+            if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean isIdenticalAfterDowncase(final String text) {
+        final int len = text.length();
+        for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+            final int codePoint = text.codePointAt(i);
+            if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
+            final String separators) {
+        boolean needCapsNext = true;
+        final int len = text.length();
+        for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+            final int codePoint = text.codePointAt(i);
+            if (Character.isLetter(codePoint)) {
+                if ((needCapsNext && !Character.isUpperCase(codePoint))
+                        || (!needCapsNext && !Character.isLowerCase(codePoint))) {
+                    return false;
+                }
+            }
+            // We need a capital letter next if this is a separator.
+            needCapsNext = (-1 != separators.indexOf(codePoint));
+        }
+        return true;
+    }
+
+    // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph
+    // which should be capitalized together in *some* cases.
+    public static String capitalizeEachWord(final String text, final String separators,
+            final Locale locale) {
+        final StringBuilder builder = new StringBuilder();
+        boolean needCapsNext = true;
+        final int len = text.length();
+        for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+            final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1));
+            if (needCapsNext) {
+                builder.append(nextChar.toUpperCase(locale));
+            } else {
+                builder.append(nextChar.toLowerCase(locale));
+            }
+            // We need a capital letter next if this is a separator.
+            needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0)));
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Approximates whether the text before the cursor looks like a URL.
+     *
+     * This is not foolproof, but it should work well in the practice.
+     * Essentially it walks backward from the cursor until it finds something that's not a letter,
+     * digit, or common URL symbol like underscore. If it hasn't found a period yet, then it
+     * does not look like a URL.
+     * If the text:
+     * - starts with www and contains a period
+     * - starts with a slash preceded by either a slash, whitespace, or start-of-string
+     * Then it looks like a URL and we return true. Otherwise, we return false.
+     *
+     * Note: this method is called quite often, and should be fast.
+     *
+     * TODO: This will return that "abc./def" and ".abc/def" look like URLs to keep down the
+     * code complexity, but ideally it should not. It's acceptable for now.
+     */
+    public static boolean lastPartLooksLikeURL(final CharSequence text) {
+        int i = text.length();
+        if (0 == i) return false;
+        int wCount = 0;
+        int slashCount = 0;
+        boolean hasSlash = false;
+        boolean hasPeriod = false;
+        int codePoint = 0;
+        while (i > 0) {
+            codePoint =  Character.codePointBefore(text, i);
+            if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') {
+                // Handwavy heuristic to see if that's a URL character. Anything between period
+                // and z. This includes all lower- and upper-case ascii letters, period,
+                // underscore, arrobase, question mark, equal sign. It excludes spaces, exclamation
+                // marks, double quotes...
+                // Anything that's not a URL-like character causes us to break from here and
+                // evaluate normally.
+                break;
+            }
+            if (Constants.CODE_PERIOD == codePoint) {
+                hasPeriod = true;
+            }
+            if (Constants.CODE_SLASH == codePoint) {
+                hasSlash = true;
+                if (2 == ++slashCount) {
+                    return true;
+                }
+            } else {
+                slashCount = 0;
+            }
+            if ('w' == codePoint) {
+                ++wCount;
+            } else {
+                wCount = 0;
+            }
+            i = Character.offsetByCodePoints(text, i, -1);
+        }
+        // End of the text run.
+        // If it starts with www and includes a period, then it looks like a URL.
+        if (wCount >= 3 && hasPeriod) return true;
+        // If it starts with a slash, and the code point before is whitespace, it looks like an URL.
+        if (1 == slashCount && (0 == i || Character.isWhitespace(codePoint))) return true;
+        // If it has both a period and a slash, it looks like an URL.
+        if (hasPeriod && hasSlash) return true;
+        // Otherwise, it doesn't look like an URL.
+        return false;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
index 5e28cc2..4d88ecc 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -183,7 +183,7 @@
             final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
             displayName = locale.getDisplayName(displayLocale);
         }
-        return StringUtils.toTitleCase(displayName, displayLocale);
+        return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale);
     }
 
     // InputMethodSubtype's display name in its locale.
@@ -243,7 +243,7 @@
                 }
             }
         };
-        return StringUtils.toTitleCase(
+        return StringUtils.capitalizeFirstCodePoint(
                 getSubtypeName.runInLocale(sResources, displayLocale), displayLocale);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 975664d..671d714 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -47,6 +47,9 @@
     // TODO: rename this to CORRECTION_ON
     public static final int CORRECTION_FULL = 1;
 
+    // Close to -2**31
+    private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000;
+
     public interface SuggestInitializationListener {
         public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
     }
@@ -334,7 +337,21 @@
             }
         }
 
+        if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord,
+                wordComposer.getRejectedBatchModeSuggestion())) {
+            final SuggestedWordInfo rejected = suggestionsContainer.remove(0);
+            suggestionsContainer.add(1, rejected);
+        }
         SuggestedWordInfo.removeDups(suggestionsContainer);
+
+        // For some reason some suggestions with MIN_VALUE are making their way here.
+        // TODO: Find a more robust way to detect distractors.
+        for (int i = suggestionsContainer.size() - 1; i >= 0; --i) {
+            if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) {
+                suggestionsContainer.remove(i);
+            }
+        }
+
         // 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,
@@ -394,7 +411,7 @@
         if (isAllUpperCase) {
             sb.append(wordInfo.mWord.toUpperCase(locale));
         } else if (isFirstCharCapitalized) {
-            sb.append(StringUtils.toTitleCase(wordInfo.mWord, locale));
+            sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale));
         } else {
             sb.append(wordInfo.mWord);
         }
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 3d6fe2d..616e191 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -131,6 +131,7 @@
         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 static final int KIND_RESUMED = 9; // A resumed suggestion (comes from a span)
         public final String mWord;
         public final int mScore;
         public final int mKind; // one of the KIND_* constants above
@@ -194,4 +195,21 @@
             }
         }
     }
+
+    // SuggestedWords is an immutable object, as much as possible. We must not just remove
+    // words from the member ArrayList as some other parties may expect the object to never change.
+    public SuggestedWords getSuggestedWordsExcludingTypedWord() {
+        final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList();
+        for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
+            final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
+            if (SuggestedWordInfo.KIND_TYPED != info.mKind) {
+                newSuggestions.add(info);
+            }
+        }
+        // We should never autocorrect, so we say the typed word is valid. Also, in this case,
+        // no auto-correction should take place hence willAutoCorrect = false.
+        return new SuggestedWords(newSuggestions, true /* typedWordValid */,
+                false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions,
+                mIsPrediction);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 0d5bde6..90f9297 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -20,7 +20,6 @@
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
-import android.content.Intent;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
@@ -28,7 +27,10 @@
 import android.provider.UserDictionary.Words;
 import android.text.TextUtils;
 
+import com.android.inputmethod.compat.UserDictionaryCompatUtils;
+
 import java.util.Arrays;
+import java.util.Locale;
 
 /**
  * An expandable dictionary that stores the words in the user dictionary provider into a binary
@@ -61,10 +63,6 @@
 
     private static final String NAME = "userunigram";
 
-    // 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;
@@ -211,23 +209,19 @@
     /**
      * 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.
      */
     public synchronized void addWordToUserDictionary(final String word) {
-        // 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() >= MAX_WORD_LENGTH) return;
-
-        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);
-        mContext.startActivity(intent);
+        // Update the user dictionary provider
+        final Locale locale;
+        if (USER_DICTIONARY_ALL_LANGUAGES == mLocale) {
+            locale = null;
+        } else {
+            locale = LocaleUtils.constructLocaleFromString(mLocale);
+        }
+        UserDictionaryCompatUtils.addWord(mContext, word,
+                HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY, null, locale);
     }
 
     private int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) {
@@ -258,10 +252,10 @@
                 final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
                 // Safeguard against adding really long words.
                 if (word.length() < MAX_WORD_LENGTH) {
-                    super.addWord(word, null, adjustedFrequency);
+                    super.addWord(word, null, adjustedFrequency, false /* isNotAWord */);
                 }
                 if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
-                    super.addWord(shortcut, word, adjustedFrequency);
+                    super.addWord(shortcut, word, adjustedFrequency, true /* isNotAWord */);
                 }
                 cursor.moveToNext();
             }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index f7cb434..51bd901 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -27,6 +27,7 @@
  */
 public final class WordComposer {
     private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
+    private static final boolean DBG = LatinImeLogger.sDBG;
 
     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
@@ -42,6 +43,13 @@
     private String mAutoCorrection;
     private boolean mIsResumed;
     private boolean mIsBatchMode;
+    // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user
+    // gestures a word, is displeased with the results and hits backspace, then gestures again.
+    // At the very least we should avoid re-suggesting the same thing, and to do that we memorize
+    // the rejected suggestion in this variable.
+    // TODO: this should be done in a comprehensive way by the User History feature instead of
+    // as an ad-hockery here.
+    private String mRejectedBatchModeSuggestion;
 
     // Cache these values for performance
     private int mCapsCount;
@@ -49,6 +57,7 @@
     private int mCapitalizedMode;
     private int mTrailingSingleQuotesCount;
     private int mCodePointSize;
+    private int mCursorPositionWithinWord;
 
     /**
      * Whether the user chose to capitalize the first char of the word.
@@ -62,6 +71,8 @@
         mTrailingSingleQuotesCount = 0;
         mIsResumed = false;
         mIsBatchMode = false;
+        mCursorPositionWithinWord = 0;
+        mRejectedBatchModeSuggestion = null;
         refreshSize();
     }
 
@@ -76,6 +87,8 @@
         mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
         mIsResumed = source.mIsResumed;
         mIsBatchMode = source.mIsBatchMode;
+        mCursorPositionWithinWord = source.mCursorPositionWithinWord;
+        mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
         refreshSize();
     }
 
@@ -91,6 +104,8 @@
         mTrailingSingleQuotesCount = 0;
         mIsResumed = false;
         mIsBatchMode = false;
+        mCursorPositionWithinWord = 0;
+        mRejectedBatchModeSuggestion = null;
         refreshSize();
     }
 
@@ -118,6 +133,13 @@
         return mPrimaryKeyCodes[index];
     }
 
+    public int getCodeBeforeCursor() {
+        if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) {
+            return Constants.NOT_A_CODE;
+        }
+        return mPrimaryKeyCodes[mCursorPositionWithinWord - 1];
+    }
+
     public InputPointers getInputPointers() {
         return mInputPointers;
     }
@@ -135,6 +157,7 @@
         final int newIndex = size();
         mTypedWord.appendCodePoint(primaryCode);
         refreshSize();
+        mCursorPositionWithinWord = mCodePointSize;
         if (newIndex < MAX_WORD_LENGTH) {
             mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE
                     ? Character.toLowerCase(primaryCode) : primaryCode;
@@ -158,6 +181,18 @@
         mAutoCorrection = null;
     }
 
+    public void setCursorPositionWithinWord(final int posWithinWord) {
+        mCursorPositionWithinWord = posWithinWord;
+    }
+
+    public boolean isCursorFrontOrMiddleOfComposingWord() {
+        if (DBG && mCursorPositionWithinWord > mCodePointSize) {
+            throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
+                    + "in a word of size " + mCodePointSize);
+        }
+        return mCursorPositionWithinWord != mCodePointSize;
+    }
+
     public void setBatchInputPointers(final InputPointers batchPointers) {
         mInputPointers.set(batchPointers);
         mIsBatchMode = true;
@@ -242,6 +277,7 @@
                 ++mTrailingSingleQuotesCount;
             }
         }
+        mCursorPositionWithinWord = mCodePointSize;
         mAutoCorrection = null;
     }
 
@@ -368,7 +404,9 @@
         mCapitalizedMode = CAPS_MODE_OFF;
         refreshSize();
         mAutoCorrection = null;
+        mCursorPositionWithinWord = 0;
         mIsResumed = false;
+        mRejectedBatchModeSuggestion = null;
         return lastComposedWord;
     }
 
@@ -380,10 +418,20 @@
         refreshSize();
         mCapitalizedMode = lastComposedWord.mCapitalizedMode;
         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
+        mCursorPositionWithinWord = mCodePointSize;
+        mRejectedBatchModeSuggestion = null;
         mIsResumed = true;
     }
 
     public boolean isBatchMode() {
         return mIsBatchMode;
     }
+
+    public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
+        mRejectedBatchModeSuggestion = rejectedSuggestion;
+    }
+
+    public String getRejectedBatchModeSuggestion() {
+        return mRejectedBatchModeSuggestion;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
index 58ec1e8..467f6a0 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -1467,8 +1467,8 @@
                     if (null == last) continue;
                     builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
                     buffer.position(last.mChildrenAddress + headerSize);
-                    groupOffset = last.mChildrenAddress + 1;
-                    i = buffer.readUnsignedByte();
+                    i = readCharGroupCount(buffer);
+                    groupOffset = last.mChildrenAddress + getGroupCountSize(i);
                     last = null;
                     continue;
                 }
@@ -1477,8 +1477,8 @@
             if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
                 builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
                 buffer.position(last.mChildrenAddress + headerSize);
-                groupOffset = last.mChildrenAddress + 1;
-                i = buffer.readUnsignedByte();
+                i = readCharGroupCount(buffer);
+                groupOffset = last.mChildrenAddress + getGroupCountSize(i);
                 last = null;
                 continue;
             }
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index e7c7e2b..17d2815 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -647,7 +647,7 @@
 
         if (index < codePoints.length) return null;
         if (!currentGroup.isTerminal()) return null;
-        if (DBG && !codePoints.equals(checker.toString())) return null;
+        if (DBG && !string.equals(checker.toString())) return null;
         return currentGroup;
     }
 
@@ -853,16 +853,19 @@
                 if (currentPos.pos.hasNext()) {
                     final CharGroup currentGroup = currentPos.pos.next();
                     currentPos.length = mCurrentString.length();
-                    for (int i : currentGroup.mChars)
+                    for (int i : currentGroup.mChars) {
                         mCurrentString.append(Character.toChars(i));
+                    }
                     if (null != currentGroup.mChildren) {
                         currentPos = new Position(currentGroup.mChildren.mData);
+                        currentPos.length = mCurrentString.length();
                         mPositions.addLast(currentPos);
                     }
-                    if (currentGroup.mFrequency >= 0)
+                    if (currentGroup.mFrequency >= 0) {
                         return new Word(mCurrentString.toString(), currentGroup.mFrequency,
                                 currentGroup.mShortcutTargets, currentGroup.mBigrams,
                                 currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry);
+                    }
                 } else {
                     mPositions.removeLast();
                     currentPos = mPositions.getLast();
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 8d3b062..2d0a89b 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -330,7 +330,7 @@
                 } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) {
                     for (int i = 0; i < mSuggestions.size(); ++i) {
                         // Likewise
-                        mSuggestions.set(i, StringUtils.toTitleCase(
+                        mSuggestions.set(i, StringUtils.capitalizeFirstCodePoint(
                                 mSuggestions.get(i).toString(), locale));
                     }
                 }
@@ -403,11 +403,7 @@
 
     public DictAndProximity createDictAndProximity(final Locale locale) {
         final int script = getScriptFromLocale(locale);
-        final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo(
-                SpellCheckerProximityInfo.getProximityForScript(script),
-                SpellCheckerProximityInfo.ROW_SIZE,
-                SpellCheckerProximityInfo.PROXIMITY_GRID_WIDTH,
-                SpellCheckerProximityInfo.PROXIMITY_GRID_HEIGHT);
+        final ProximityInfo proximityInfo = new SpellCheckerProximityInfo(script);
         final DictionaryCollection dictionaryCollection =
                 DictionaryFactory.createMainDictionaryFromManager(this, locale,
                         true /* useFullEditDistance */);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index b150632..da86572 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -189,10 +189,12 @@
         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
+            // Any word containing a COMMERCIAL_AT is probably an e-mail address
+            // Any word containing a SLASH is probably either an ad-hoc combination of two
             // words or a URI - in either case we don't want to spell check that
-            if ('@' == codePoint || '/' == codePoint) return true;
+            if (Constants.CODE_COMMERCIAL_AT == codePoint || Constants.CODE_SLASH == codePoint) {
+                return true;
+            }
             if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
         }
         // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
@@ -226,7 +228,7 @@
         // If the lower case version is not in the dictionary, it's still possible
         // that we have an all-caps version of a word that needs to be capitalized
         // according to the dictionary. E.g. "GERMANS" only exists in the dictionary as "Germans".
-        return dict.isValidWord(StringUtils.toTitleCase(lowerCaseText, mLocale));
+        return dict.isValidWord(StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale));
     }
 
     // Note : this must be reentrant
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
index 49dca21..0c480ea 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
@@ -16,53 +16,40 @@
 
 package com.android.inputmethod.latin.spellcheck;
 
-import com.android.inputmethod.annotations.UsedForTesting;
+import android.util.SparseIntArray;
+
 import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.Constants;
 
-import java.util.TreeMap;
+public final class SpellCheckerProximityInfo extends ProximityInfo {
+    public SpellCheckerProximityInfo(final int script) {
+        super(getProximityForScript(script), PROXIMITY_GRID_WIDTH, PROXIMITY_GRID_HEIGHT);
+    }
 
-public final class SpellCheckerProximityInfo {
-    @UsedForTesting
-    final public static int NUL = Constants.NOT_A_CODE;
+    private static final 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
     // as the size of the passed array afterwards so they can't be different.
-    final public static int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
+    private static final int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
 
     // The number of keys in a row of the grid used by the spell checker.
-    final public static int PROXIMITY_GRID_WIDTH = 11;
+    private static final int PROXIMITY_GRID_WIDTH = 11;
     // The number of rows in the grid used by the spell checker.
-    final public static int PROXIMITY_GRID_HEIGHT = 3;
+    private static final int PROXIMITY_GRID_HEIGHT = 3;
 
-    final private static int NOT_AN_INDEX = -1;
-    final public static int NOT_A_COORDINATE_PAIR = -1;
+    private static final int NOT_AN_INDEX = -1;
+    public static final int NOT_A_COORDINATE_PAIR = -1;
 
     // Helper methods
-    final protected static void buildProximityIndices(final int[] proximity,
-            final TreeMap<Integer, Integer> indices) {
-        for (int i = 0; i < proximity.length; i += ROW_SIZE) {
-            if (NUL != proximity[i]) indices.put(proximity[i], i / ROW_SIZE);
+    static void buildProximityIndices(final int[] proximity, final int rowSize,
+            final SparseIntArray indices) {
+        for (int i = 0; i < proximity.length; i += rowSize) {
+            if (NUL != proximity[i]) indices.put(proximity[i], i / rowSize);
         }
     }
-    final protected static int computeIndex(final int characterCode,
-            final TreeMap<Integer, Integer> indices) {
-        final Integer result = indices.get(characterCode);
-        if (null == result) return NOT_AN_INDEX;
-        return result;
-    }
 
     private static final class Latin {
-        // This is a map from the code point to the index in the PROXIMITY array.
-        // At the time the native code to read the binary dictionary needs the proximity info be
-        // passed as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input
-        // 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 = CollectionUtils.newTreeMap();
-
         // The proximity here is the union of
         // - the proximity for a QWERTY keyboard.
         // - the proximity for an AZERTY keyboard.
@@ -79,7 +66,7 @@
              a s d f g h j k l
                z x c v b n m
         */
-        final static int[] PROXIMITY = {
+        static final int[] PROXIMITY = {
             // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
             // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
             // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
@@ -121,16 +108,21 @@
             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,
         };
+
+        // This is a mapping array from the code point to the index in the PROXIMITY array.
+        // When we check the spelling of a word, we need to pass (x,y) coordinates to the native
+        // code for each letter of the word. These are most easily computed from the index in the
+        // PROXIMITY array. Since we'll need to do that very often, the index lookup from the code
+        // point needs to be as fast as possible, and a map is probably the best way to do this.
+        // To avoid unnecessary boxing conversion to Integer, here we use SparseIntArray.
+        static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
+
         static {
-            buildProximityIndices(PROXIMITY, INDICES);
-        }
-        static int getIndexOf(int characterCode) {
-            return computeIndex(characterCode, INDICES);
+            buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
         }
     }
 
     private static final class Cyrillic {
-        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.
         /*
@@ -207,7 +199,7 @@
         private static final int CY_SOFT_SIGN = '\u044C'; // ь
         private static final int CY_BE = '\u0431'; // б
         private static final int CY_YU = '\u044E'; // ю
-        final static int[] PROXIMITY = {
+        static final int[] PROXIMITY = {
             // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
             // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
             // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
@@ -280,16 +272,15 @@
             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 final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
+
         static {
-            buildProximityIndices(PROXIMITY, INDICES);
-        }
-        static int getIndexOf(int characterCode) {
-            return computeIndex(characterCode, INDICES);
+            buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
         }
     }
 
     private static final class Greek {
-        final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap();
         // TODO: The following table is solely based on the keyboard layout. Consult with Greek
         // speakers on commonly misspelled words/letters.
         /*
@@ -354,7 +345,7 @@
         private static final int GR_BETA = '\u03B2'; // β
         private static final int GR_NU = '\u03BD'; // ν
         private static final int GR_MU = '\u03BC'; // μ
-        final static int[] PROXIMITY = {
+        static final int[] PROXIMITY = {
             // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
             // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
             // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
@@ -419,37 +410,37 @@
             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 final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
+
         static {
-            buildProximityIndices(PROXIMITY, INDICES);
-        }
-        static int getIndexOf(int characterCode) {
-            return computeIndex(characterCode, INDICES);
+            buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
         }
     }
 
-    public static int[] getProximityForScript(final int script) {
+    private static int[] getProximityForScript(final int script) {
         switch (script) {
-            case AndroidSpellCheckerService.SCRIPT_LATIN:
-                return Latin.PROXIMITY;
-            case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
-                return Cyrillic.PROXIMITY;
-            case AndroidSpellCheckerService.SCRIPT_GREEK:
-                return Greek.PROXIMITY;
-            default:
-                throw new RuntimeException("Wrong script supplied: " + script);
+        case AndroidSpellCheckerService.SCRIPT_LATIN:
+            return Latin.PROXIMITY;
+        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+            return Cyrillic.PROXIMITY;
+        case AndroidSpellCheckerService.SCRIPT_GREEK:
+            return Greek.PROXIMITY;
+        default:
+            throw new RuntimeException("Wrong script supplied: " + script);
         }
     }
 
     private static int getIndexOfCodeForScript(final int codePoint, final int script) {
         switch (script) {
-            case AndroidSpellCheckerService.SCRIPT_LATIN:
-                return Latin.getIndexOf(codePoint);
-            case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
-                return Cyrillic.getIndexOf(codePoint);
-            case AndroidSpellCheckerService.SCRIPT_GREEK:
-                return Greek.getIndexOf(codePoint);
-            default:
-                throw new RuntimeException("Wrong script supplied: " + script);
+        case AndroidSpellCheckerService.SCRIPT_LATIN:
+            return Latin.INDICES.get(codePoint, NOT_AN_INDEX);
+        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+            return Cyrillic.INDICES.get(codePoint, NOT_AN_INDEX);
+        case AndroidSpellCheckerService.SCRIPT_GREEK:
+            return Greek.INDICES.get(codePoint, NOT_AN_INDEX);
+        default:
+            throw new RuntimeException("Wrong script supplied: " + script);
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index ed408bb..3037669 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -16,12 +16,14 @@
 
 package com.android.inputmethod.latin.suggestions;
 
+import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.TypefaceUtils;
 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
 import com.android.inputmethod.keyboard.internal.KeyboardParams;
@@ -50,16 +52,12 @@
             super();
         }
 
-        // TODO: Remove {@link MoreSuggestionsView} argument.
         public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth,
-                final int minWidth, final int maxRow, final MoreSuggestionsView view) {
+                final int minWidth, final int maxRow, final Paint paint, final Resources res) {
             clearKeys();
-            final Resources res = view.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();
+            final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding);
 
             int row = 0;
             int pos = fromPos, rowStartPos = fromPos;
@@ -67,7 +65,7 @@
             while (pos < size) {
                 final String word = suggestions.getWord(pos);
                 // TODO: Should take care of text x-scaling.
-                mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding;
+                mWidths[pos] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding);
                 final int numColumn = pos - rowStartPos + 1;
                 final int columnWidth =
                         (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
@@ -169,8 +167,8 @@
         private int mFromPos;
         private int mToPos;
 
-        public Builder(final MoreSuggestionsView paneView) {
-            super(paneView.getContext(), new MoreSuggestionsParam());
+        public Builder(final Context context, final MoreSuggestionsView paneView) {
+            super(context, new MoreSuggestionsParam());
             mPaneView = paneView;
         }
 
@@ -183,7 +181,7 @@
 
             mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight);
             final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow,
-                    mPaneView);
+                    mPaneView.newLabelPaint(null /* key */), mResources);
             mFromPos = fromPos;
             mToPos = fromPos + count;
             mSuggestions = suggestions;
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index 438820d..6509f39 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -43,7 +43,13 @@
     }
 
     public void updateKeyboardGeometry(final int keyHeight) {
-        mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
+        updateKeyDrawParams(keyHeight);
+    }
+
+    public void adjustVerticalCorrectionForModalMode() {
+        // Set vertical correction to zero (Reset more keys keyboard sliding allowance
+        // {@link R#dimen.more_keys_keyboard_slide_allowance}).
+        mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop());
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index eeaf828..2a21ec2 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -596,7 +596,7 @@
         mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
         mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
                 .findViewById(R.id.more_suggestions_view);
-        mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView);
+        mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView);
 
         final Resources res = context.getResources();
         mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
@@ -755,8 +755,7 @@
 
     @Override
     public boolean dispatchTouchEvent(final MotionEvent me) {
-        if (!mMoreSuggestionsView.isShowingInParent()
-                || mMoreSuggestionsMode == MORE_SUGGESTIONS_IN_MODAL_MODE) {
+        if (!mMoreSuggestionsView.isShowingInParent()) {
             mLastX = (int)me.getX();
             mLastY = (int)me.getY();
             if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) {
@@ -784,6 +783,7 @@
             } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
                 // Decided to be in the modal input mode
                 mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
+                mMoreSuggestionsView.adjustVerticalCorrectionForModalMode();
             }
             return true;
         }
diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
index c5f0959..4f86526 100644
--- a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
+++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
@@ -25,9 +25,10 @@
  */
 public final class BootBroadcastReceiver extends BroadcastReceiver {
     @Override
-    public void onReceive(Context context, Intent intent) {
+    public void onReceive(final Context context, final Intent intent) {
         if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
-            ResearchLogger.scheduleUploadingService(context);
+            UploaderService.cancelAndRescheduleUploadingService(context,
+                    true /* needsRescheduling */);
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
index e1cc2da..fbfd9b5 100644
--- a/java/src/com/android/inputmethod/research/MotionEventReader.java
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -22,6 +22,7 @@
 import android.view.MotionEvent.PointerCoords;
 import android.view.MotionEvent.PointerProperties;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.BufferedReader;
@@ -64,6 +65,7 @@
         return replayData;
     }
 
+    @UsedForTesting
     static class ReplayData {
         final ArrayList<Integer> mActions = new ArrayList<Integer>();
         final ArrayList<PointerProperties[]> mPointerPropertiesArrays
@@ -134,6 +136,7 @@
      * },
      * </pre>
      */
+    @UsedForTesting
     /* package for test */ void readLogStatement(final JsonReader jsonReader,
             final ReplayData replayData) throws IOException {
         String logStatementType = null;
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index fa124f3..7a23ddb 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -20,16 +20,13 @@
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
-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.content.res.Resources;
@@ -74,22 +71,17 @@
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.research.MotionEventReader.ReplayData;
 
-import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.nio.MappedByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.charset.Charset;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
-import java.util.Locale;
 import java.util.Random;
-import java.util.UUID;
+import java.util.regex.Pattern;
 
 /**
  * Logs the use of the LatinIME keyboard.
@@ -254,7 +246,8 @@
         mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
         mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            scheduleUploadingService(mLatinIME);
+            UploaderService.cancelAndRescheduleUploadingService(mLatinIME,
+                    true /* needsRescheduling */);
         }
         mReplayer.setKeyboardSwitcher(keyboardSwitcher);
     }
@@ -268,25 +261,6 @@
         ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now);
     }
 
-    /**
-     * 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);
-    }
-
     public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
         mMainKeyboardView = mainKeyboardView;
         maybeShowSplashScreen();
@@ -406,6 +380,9 @@
         requestIndicatorRedraw();
         mStatistics.reset();
         checkForEmptyEditor();
+        if (mFeedbackLogBuffer == null) {
+            resetFeedbackLogging();
+        }
         if (!isAllowedToLog()) {
             // Log.w(TAG, "not in usability mode; not logging");
             return;
@@ -439,9 +416,6 @@
                 }
             };
         }
-        if (mFeedbackLogBuffer == null) {
-            resetFeedbackLogging();
-        }
     }
 
     private void resetFeedbackLogging() {
@@ -1092,7 +1066,7 @@
             new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
                     "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
                     "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything",
-                    "isUsingDevelopmentOnlyDiagnosticsDebug");
+                    "isDevTeamBuild");
     public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
             final SharedPreferences prefs) {
         final ResearchLogger researchLogger = getInstance();
@@ -1114,13 +1088,32 @@
                         Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
                         Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
                         OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
-                        ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG);
-            } catch (NameNotFoundException e) {
-                e.printStackTrace();
+                        researchLogger.isDevTeamBuild());
+                // Commit the logUnit so the LatinImeOnStartInputViewInternal event is in its own
+                // logUnit at the beginning of the log.
+                researchLogger.commitCurrentLogUnit();
+            } catch (final NameNotFoundException e) {
+                Log.e(TAG, "NameNotFound", e);
             }
         }
     }
 
+    // TODO: Update this heuristic pattern to something more reliable.  Developer builds tend to
+    // have the developer name and year embedded.
+    private static final Pattern developerBuildRegex = Pattern.compile("[A-Za-z]\\.20[1-9]");
+    private boolean isDevTeamBuild() {
+        try {
+            final PackageInfo packageInfo;
+            packageInfo = mLatinIME.getPackageManager().getPackageInfo(mLatinIME.getPackageName(),
+                    0);
+            final String versionName = packageInfo.versionName;
+            return !(developerBuildRegex.matcher(versionName).find());
+        } catch (final NameNotFoundException e) {
+            Log.e(TAG, "Could not determine package name", e);
+            return false;
+        }
+    }
+
     /**
      * Log a change in preferences.
      *
@@ -1290,7 +1283,7 @@
         if (connection != null) {
             Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
             if (range != null) {
-                word = range.mWord;
+                word = range.mWord.toString();
             }
         }
         final ResearchLogger researchLogger = getInstance();
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index 6a9f5c1..6a9717b 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -18,6 +18,8 @@
 
 import android.app.AlarmManager;
 import android.app.IntentService;
+import android.app.PendingIntent;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 
@@ -43,11 +45,17 @@
 
     @Override
     protected void onHandleIntent(final Intent intent) {
+        // We may reach this point either because the alarm fired, or because the system explicitly
+        // requested that an Upload occur.  In the latter case, we want to cancel the alarm in case
+        // it's about to fire.
+        cancelAndRescheduleUploadingService(this, false /* needsRescheduling */);
+
         final Uploader uploader = new Uploader(this);
         if (!uploader.isPossibleToUpload()) return;
         if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) {
             uploader.doUpload();
         }
+        cancelAndRescheduleUploadingService(this, true /* needsRescheduling */);
     }
 
     private boolean isUploadingUnconditionally(final Bundle bundle) {
@@ -57,4 +65,42 @@
         }
         return false;
     }
+
+    /**
+     * Arrange for the UploaderService to be run on a regular basis.
+     *
+     * Any existing scheduled invocation of UploaderService is removed and optionally rescheduled.
+     * This may cause problems if this method is called so often that no scheduled invocation is
+     * ever run.  But if the delay is short enough that it will go off when the user is sleeping,
+     * then there should be no starvation.
+     *
+     * @param context {@link Context} object
+     * @param needsRescheduling whether to schedule a future intent to be delivered to this service
+     */
+    public static void cancelAndRescheduleUploadingService(final Context context,
+            final boolean needsRescheduling) {
+        final PendingIntent pendingIntent = getPendingIntentForService(context);
+        final AlarmManager alarmManager = (AlarmManager) context.getSystemService(
+                Context.ALARM_SERVICE);
+        cancelAnyScheduledServiceAlarm(alarmManager, pendingIntent);
+        if (needsRescheduling) {
+            scheduleServiceAlarm(alarmManager, pendingIntent);
+        }
+    }
+
+    private static PendingIntent getPendingIntentForService(final Context context) {
+        final Intent intent = new Intent(context, UploaderService.class);
+        return PendingIntent.getService(context, 0, intent, 0);
+    }
+
+    private static void cancelAnyScheduledServiceAlarm(final AlarmManager alarmManager,
+            final PendingIntent pendingIntent) {
+        alarmManager.cancel(pendingIntent);
+    }
+
+    private static void scheduleServiceAlarm(final AlarmManager alarmManager,
+            final PendingIntent pendingIntent) {
+        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, UploaderService.RUN_INTERVAL,
+                pendingIntent);
+    }
 }
diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
index 3c482ca..dedb02a 100644
--- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
+++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
@@ -26,13 +26,13 @@
 
 static jlong latinime_Keyboard_setProximityInfo(JNIEnv *env, jclass clazz, jstring localeJStr,
         jint displayWidth, jint displayHeight, jint gridWidth, jint gridHeight,
-        jint mostCommonkeyWidth, jintArray proximityChars, jint keyCount,
+        jint mostCommonkeyWidth, jint mostCommonkeyHeight, 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, displayWidth, displayHeight,
-            gridWidth, gridHeight, mostCommonkeyWidth, proximityChars, keyCount,
-            keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
+            gridWidth, gridHeight, mostCommonkeyWidth, mostCommonkeyHeight, proximityChars,
+            keyCount, keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
             sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
     return reinterpret_cast<jlong>(proximityInfo);
 }
@@ -44,7 +44,7 @@
 
 static JNINativeMethod sMethods[] = {
     {const_cast<char *>("setProximityInfoNative"),
-     const_cast<char *>("(Ljava/lang/String;IIIII[II[I[I[I[I[I[F[F[F)J"),
+     const_cast<char *>("(Ljava/lang/String;IIIIII[II[I[I[I[I[I[F[F[F)J"),
      reinterpret_cast<void *>(latinime_Keyboard_setProximityInfo)},
     {const_cast<char *>("releaseProximityInfoNative"),
      const_cast<char *>("(J)V"),
diff --git a/native/jni/src/binary_format.h b/native/jni/src/binary_format.h
index 1c4061f..2d2e195 100644
--- a/native/jni/src/binary_format.h
+++ b/native/jni/src/binary_format.h
@@ -92,6 +92,7 @@
             const int unigramProbability, const int bigramProbability);
     static int getProbability(const int position, const std::map<int, int> *bigramMap,
             const uint8_t *bigramFilter, const int unigramProbability);
+    static float getMultiWordCostMultiplier(const uint8_t *const dict);
 
     // Flags for special processing
     // Those *must* match the flags in makedict (BinaryDictInputOutput#*_PROCESSING_FLAG) or
@@ -241,6 +242,17 @@
     return ((msb & 0x7F) << 8) | dict[(*pos)++];
 }
 
+inline float BinaryFormat::getMultiWordCostMultiplier(const uint8_t *const dict) {
+    const int headerValue = readHeaderValueInt(dict, "MULTIPLE_WORDS_DEMOTION_RATE");
+    if (headerValue == S_INT_MIN) {
+        return 1.0f;
+    }
+    if (headerValue <= 0) {
+        return static_cast<float>(MAX_VALUE_FOR_WEIGHTING);
+    }
+    return 100.0f / static_cast<float>(headerValue);
+}
+
 inline uint8_t BinaryFormat::getFlagsAndForwardPointer(const uint8_t *const dict, int *pos) {
     return dict[(*pos)++];
 }
diff --git a/native/jni/src/char_utils.cpp b/native/jni/src/char_utils.cpp
index 8d917ea..e219beb 100644
--- a/native/jni/src/char_utils.cpp
+++ b/native/jni/src/char_utils.cpp
@@ -45,18 +45,16 @@
 
 extern "C" int main() {
     for (unsigned short c = 0; c < 0xFFFF; c++) {
-        const unsigned short baseC = c < NELEMS(BASE_CHARS) ? BASE_CHARS[c] : c;
-        if (baseC <= 0x7F) continue;
-        const unsigned short icu4cLowerBaseC = u_tolower(baseC);
-        const unsigned short myLowerBaseC = latin_tolower(baseC);
-        if (baseC != icu4cLowerBaseC) {
+        if (c <= 0x7F) continue;
+        const unsigned short icu4cLowerC = u_tolower(c);
+        const unsigned short myLowerC = latin_tolower(c);
+        if (c != icu4cLowerC) {
 #ifdef CONFIRMING_CHAR_UTILS
-            if (icu4cLowerBaseC != myLowerBaseC) {
-                fprintf(stderr, "icu4cLowerBaseC != myLowerBaseC, 0x%04X, 0x%04X\n",
-                        icu4cLowerBaseC, myLowerBaseC);
+            if (icu4cLowerC != myLowerC) {
+                fprintf(stderr, "icu4cLowerC != myLowerC, 0x%04X, 0x%04X\n", icu4cLowerC, myLowerC);
             }
 #else // CONFIRMING_CHAR_UTILS
-            printf("0x%04X, 0x%04X\n", baseC, icu4cLowerBaseC);
+            printf("0x%04X, 0x%04X\n", c, icu4cLowerC);
 #endif // CONFIRMING_CHAR_UTILS
         }
     }
@@ -77,14 +75,99 @@
  *    $
  */
 static const struct LatinCapitalSmallPair SORTED_CHAR_MAP[] = {
+    { 0x00C0, 0x00E0 },  // LATIN CAPITAL LETTER A WITH GRAVE
+    { 0x00C1, 0x00E1 },  // LATIN CAPITAL LETTER A WITH ACUTE
+    { 0x00C2, 0x00E2 },  // LATIN CAPITAL LETTER A WITH CIRCUMFLEX
+    { 0x00C3, 0x00E3 },  // LATIN CAPITAL LETTER A WITH TILDE
+    { 0x00C4, 0x00E4 },  // LATIN CAPITAL LETTER A WITH DIAERESIS
+    { 0x00C5, 0x00E5 },  // LATIN CAPITAL LETTER A WITH RING ABOVE
     { 0x00C6, 0x00E6 },  // LATIN CAPITAL LETTER AE
+    { 0x00C7, 0x00E7 },  // LATIN CAPITAL LETTER C WITH CEDILLA
+    { 0x00C8, 0x00E8 },  // LATIN CAPITAL LETTER E WITH GRAVE
+    { 0x00C9, 0x00E9 },  // LATIN CAPITAL LETTER E WITH ACUTE
+    { 0x00CA, 0x00EA },  // LATIN CAPITAL LETTER E WITH CIRCUMFLEX
+    { 0x00CB, 0x00EB },  // LATIN CAPITAL LETTER E WITH DIAERESIS
+    { 0x00CC, 0x00EC },  // LATIN CAPITAL LETTER I WITH GRAVE
+    { 0x00CD, 0x00ED },  // LATIN CAPITAL LETTER I WITH ACUTE
+    { 0x00CE, 0x00EE },  // LATIN CAPITAL LETTER I WITH CIRCUMFLEX
+    { 0x00CF, 0x00EF },  // LATIN CAPITAL LETTER I WITH DIAERESIS
     { 0x00D0, 0x00F0 },  // LATIN CAPITAL LETTER ETH
+    { 0x00D1, 0x00F1 },  // LATIN CAPITAL LETTER N WITH TILDE
+    { 0x00D2, 0x00F2 },  // LATIN CAPITAL LETTER O WITH GRAVE
+    { 0x00D3, 0x00F3 },  // LATIN CAPITAL LETTER O WITH ACUTE
+    { 0x00D4, 0x00F4 },  // LATIN CAPITAL LETTER O WITH CIRCUMFLEX
+    { 0x00D5, 0x00F5 },  // LATIN CAPITAL LETTER O WITH TILDE
+    { 0x00D6, 0x00F6 },  // LATIN CAPITAL LETTER O WITH DIAERESIS
+    { 0x00D8, 0x00F8 },  // LATIN CAPITAL LETTER O WITH STROKE
+    { 0x00D9, 0x00F9 },  // LATIN CAPITAL LETTER U WITH GRAVE
+    { 0x00DA, 0x00FA },  // LATIN CAPITAL LETTER U WITH ACUTE
+    { 0x00DB, 0x00FB },  // LATIN CAPITAL LETTER U WITH CIRCUMFLEX
+    { 0x00DC, 0x00FC },  // LATIN CAPITAL LETTER U WITH DIAERESIS
+    { 0x00DD, 0x00FD },  // LATIN CAPITAL LETTER Y WITH ACUTE
     { 0x00DE, 0x00FE },  // LATIN CAPITAL LETTER THORN
+    { 0x0100, 0x0101 },  // LATIN CAPITAL LETTER A WITH MACRON
+    { 0x0102, 0x0103 },  // LATIN CAPITAL LETTER A WITH BREVE
+    { 0x0104, 0x0105 },  // LATIN CAPITAL LETTER A WITH OGONEK
+    { 0x0106, 0x0107 },  // LATIN CAPITAL LETTER C WITH ACUTE
+    { 0x0108, 0x0109 },  // LATIN CAPITAL LETTER C WITH CIRCUMFLEX
+    { 0x010A, 0x010B },  // LATIN CAPITAL LETTER C WITH DOT ABOVE
+    { 0x010C, 0x010D },  // LATIN CAPITAL LETTER C WITH CARON
+    { 0x010E, 0x010F },  // LATIN CAPITAL LETTER D WITH CARON
     { 0x0110, 0x0111 },  // LATIN CAPITAL LETTER D WITH STROKE
+    { 0x0112, 0x0113 },  // LATIN CAPITAL LETTER E WITH MACRON
+    { 0x0114, 0x0115 },  // LATIN CAPITAL LETTER E WITH BREVE
+    { 0x0116, 0x0117 },  // LATIN CAPITAL LETTER E WITH DOT ABOVE
+    { 0x0118, 0x0119 },  // LATIN CAPITAL LETTER E WITH OGONEK
+    { 0x011A, 0x011B },  // LATIN CAPITAL LETTER E WITH CARON
+    { 0x011C, 0x011D },  // LATIN CAPITAL LETTER G WITH CIRCUMFLEX
+    { 0x011E, 0x011F },  // LATIN CAPITAL LETTER G WITH BREVE
+    { 0x0120, 0x0121 },  // LATIN CAPITAL LETTER G WITH DOT ABOVE
+    { 0x0122, 0x0123 },  // LATIN CAPITAL LETTER G WITH CEDILLA
+    { 0x0124, 0x0125 },  // LATIN CAPITAL LETTER H WITH CIRCUMFLEX
     { 0x0126, 0x0127 },  // LATIN CAPITAL LETTER H WITH STROKE
+    { 0x0128, 0x0129 },  // LATIN CAPITAL LETTER I WITH TILDE
+    { 0x012A, 0x012B },  // LATIN CAPITAL LETTER I WITH MACRON
+    { 0x012C, 0x012D },  // LATIN CAPITAL LETTER I WITH BREVE
+    { 0x012E, 0x012F },  // LATIN CAPITAL LETTER I WITH OGONEK
+    { 0x0130, 0x0069 },  // LATIN CAPITAL LETTER I WITH DOT ABOVE
+    { 0x0132, 0x0133 },  // LATIN CAPITAL LIGATURE IJ
+    { 0x0134, 0x0135 },  // LATIN CAPITAL LETTER J WITH CIRCUMFLEX
+    { 0x0136, 0x0137 },  // LATIN CAPITAL LETTER K WITH CEDILLA
+    { 0x0139, 0x013A },  // LATIN CAPITAL LETTER L WITH ACUTE
+    { 0x013B, 0x013C },  // LATIN CAPITAL LETTER L WITH CEDILLA
+    { 0x013D, 0x013E },  // LATIN CAPITAL LETTER L WITH CARON
+    { 0x013F, 0x0140 },  // LATIN CAPITAL LETTER L WITH MIDDLE DOT
+    { 0x0141, 0x0142 },  // LATIN CAPITAL LETTER L WITH STROKE
+    { 0x0143, 0x0144 },  // LATIN CAPITAL LETTER N WITH ACUTE
+    { 0x0145, 0x0146 },  // LATIN CAPITAL LETTER N WITH CEDILLA
+    { 0x0147, 0x0148 },  // LATIN CAPITAL LETTER N WITH CARON
     { 0x014A, 0x014B },  // LATIN CAPITAL LETTER ENG
+    { 0x014C, 0x014D },  // LATIN CAPITAL LETTER O WITH MACRON
+    { 0x014E, 0x014F },  // LATIN CAPITAL LETTER O WITH BREVE
+    { 0x0150, 0x0151 },  // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE
     { 0x0152, 0x0153 },  // LATIN CAPITAL LIGATURE OE
+    { 0x0154, 0x0155 },  // LATIN CAPITAL LETTER R WITH ACUTE
+    { 0x0156, 0x0157 },  // LATIN CAPITAL LETTER R WITH CEDILLA
+    { 0x0158, 0x0159 },  // LATIN CAPITAL LETTER R WITH CARON
+    { 0x015A, 0x015B },  // LATIN CAPITAL LETTER S WITH ACUTE
+    { 0x015C, 0x015D },  // LATIN CAPITAL LETTER S WITH CIRCUMFLEX
+    { 0x015E, 0x015F },  // LATIN CAPITAL LETTER S WITH CEDILLA
+    { 0x0160, 0x0161 },  // LATIN CAPITAL LETTER S WITH CARON
+    { 0x0162, 0x0163 },  // LATIN CAPITAL LETTER T WITH CEDILLA
+    { 0x0164, 0x0165 },  // LATIN CAPITAL LETTER T WITH CARON
     { 0x0166, 0x0167 },  // LATIN CAPITAL LETTER T WITH STROKE
+    { 0x0168, 0x0169 },  // LATIN CAPITAL LETTER U WITH TILDE
+    { 0x016A, 0x016B },  // LATIN CAPITAL LETTER U WITH MACRON
+    { 0x016C, 0x016D },  // LATIN CAPITAL LETTER U WITH BREVE
+    { 0x016E, 0x016F },  // LATIN CAPITAL LETTER U WITH RING ABOVE
+    { 0x0170, 0x0171 },  // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE
+    { 0x0172, 0x0173 },  // LATIN CAPITAL LETTER U WITH OGONEK
+    { 0x0174, 0x0175 },  // LATIN CAPITAL LETTER W WITH CIRCUMFLEX
+    { 0x0176, 0x0177 },  // LATIN CAPITAL LETTER Y WITH CIRCUMFLEX
+    { 0x0178, 0x00FF },  // LATIN CAPITAL LETTER Y WITH DIAERESIS
+    { 0x0179, 0x017A },  // LATIN CAPITAL LETTER Z WITH ACUTE
+    { 0x017B, 0x017C },  // LATIN CAPITAL LETTER Z WITH DOT ABOVE
+    { 0x017D, 0x017E },  // LATIN CAPITAL LETTER Z WITH CARON
     { 0x0181, 0x0253 },  // LATIN CAPITAL LETTER B WITH HOOK
     { 0x0182, 0x0183 },  // LATIN CAPITAL LETTER B WITH TOPBAR
     { 0x0184, 0x0185 },  // LATIN CAPITAL LETTER TONE SIX
@@ -105,6 +188,7 @@
     { 0x019C, 0x026F },  // LATIN CAPITAL LETTER TURNED M
     { 0x019D, 0x0272 },  // LATIN CAPITAL LETTER N WITH LEFT HOOK
     { 0x019F, 0x0275 },  // LATIN CAPITAL LETTER O WITH MIDDLE TILDE
+    { 0x01A0, 0x01A1 },  // LATIN CAPITAL LETTER O WITH HORN
     { 0x01A2, 0x01A3 },  // LATIN CAPITAL LETTER OI
     { 0x01A4, 0x01A5 },  // LATIN CAPITAL LETTER P WITH HOOK
     { 0x01A6, 0x0280 },  // LATIN LETTER YR
@@ -112,6 +196,7 @@
     { 0x01A9, 0x0283 },  // LATIN CAPITAL LETTER ESH
     { 0x01AC, 0x01AD },  // LATIN CAPITAL LETTER T WITH HOOK
     { 0x01AE, 0x0288 },  // LATIN CAPITAL LETTER T WITH RETROFLEX HOOK
+    { 0x01AF, 0x01B0 },  // LATIN CAPITAL LETTER U WITH HORN
     { 0x01B1, 0x028A },  // LATIN CAPITAL LETTER UPSILON
     { 0x01B2, 0x028B },  // LATIN CAPITAL LETTER V WITH HOOK
     { 0x01B3, 0x01B4 },  // LATIN CAPITAL LETTER Y WITH HOOK
@@ -119,13 +204,64 @@
     { 0x01B7, 0x0292 },  // LATIN CAPITAL LETTER EZH
     { 0x01B8, 0x01B9 },  // LATIN CAPITAL LETTER EZH REVERSED
     { 0x01BC, 0x01BD },  // LATIN CAPITAL LETTER TONE FIVE
+    { 0x01C4, 0x01C6 },  // LATIN CAPITAL LETTER DZ WITH CARON
+    { 0x01C5, 0x01C6 },  // LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON
+    { 0x01C7, 0x01C9 },  // LATIN CAPITAL LETTER LJ
+    { 0x01C8, 0x01C9 },  // LATIN CAPITAL LETTER L WITH SMALL LETTER J
+    { 0x01CA, 0x01CC },  // LATIN CAPITAL LETTER NJ
+    { 0x01CB, 0x01CC },  // LATIN CAPITAL LETTER N WITH SMALL LETTER J
+    { 0x01CD, 0x01CE },  // LATIN CAPITAL LETTER A WITH CARON
+    { 0x01CF, 0x01D0 },  // LATIN CAPITAL LETTER I WITH CARON
+    { 0x01D1, 0x01D2 },  // LATIN CAPITAL LETTER O WITH CARON
+    { 0x01D3, 0x01D4 },  // LATIN CAPITAL LETTER U WITH CARON
+    { 0x01D5, 0x01D6 },  // LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON
+    { 0x01D7, 0x01D8 },  // LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE
+    { 0x01D9, 0x01DA },  // LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON
+    { 0x01DB, 0x01DC },  // LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE
+    { 0x01DE, 0x01DF },  // LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON
+    { 0x01E0, 0x01E1 },  // LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON
+    { 0x01E2, 0x01E3 },  // LATIN CAPITAL LETTER AE WITH MACRON
     { 0x01E4, 0x01E5 },  // LATIN CAPITAL LETTER G WITH STROKE
+    { 0x01E6, 0x01E7 },  // LATIN CAPITAL LETTER G WITH CARON
+    { 0x01E8, 0x01E9 },  // LATIN CAPITAL LETTER K WITH CARON
+    { 0x01EA, 0x01EB },  // LATIN CAPITAL LETTER O WITH OGONEK
+    { 0x01EC, 0x01ED },  // LATIN CAPITAL LETTER O WITH OGONEK AND MACRON
+    { 0x01EE, 0x01EF },  // LATIN CAPITAL LETTER EZH WITH CARON
+    { 0x01F1, 0x01F3 },  // LATIN CAPITAL LETTER DZ
+    { 0x01F2, 0x01F3 },  // LATIN CAPITAL LETTER D WITH SMALL LETTER Z
+    { 0x01F4, 0x01F5 },  // LATIN CAPITAL LETTER G WITH ACUTE
     { 0x01F6, 0x0195 },  // LATIN CAPITAL LETTER HWAIR
     { 0x01F7, 0x01BF },  // LATIN CAPITAL LETTER WYNN
+    { 0x01F8, 0x01F9 },  // LATIN CAPITAL LETTER N WITH GRAVE
+    { 0x01FA, 0x01FB },  // LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE
+    { 0x01FC, 0x01FD },  // LATIN CAPITAL LETTER AE WITH ACUTE
+    { 0x01FE, 0x01FF },  // LATIN CAPITAL LETTER O WITH STROKE AND ACUTE
+    { 0x0200, 0x0201 },  // LATIN CAPITAL LETTER A WITH DOUBLE GRAVE
+    { 0x0202, 0x0203 },  // LATIN CAPITAL LETTER A WITH INVERTED BREVE
+    { 0x0204, 0x0205 },  // LATIN CAPITAL LETTER E WITH DOUBLE GRAVE
+    { 0x0206, 0x0207 },  // LATIN CAPITAL LETTER E WITH INVERTED BREVE
+    { 0x0208, 0x0209 },  // LATIN CAPITAL LETTER I WITH DOUBLE GRAVE
+    { 0x020A, 0x020B },  // LATIN CAPITAL LETTER I WITH INVERTED BREVE
+    { 0x020C, 0x020D },  // LATIN CAPITAL LETTER O WITH DOUBLE GRAVE
+    { 0x020E, 0x020F },  // LATIN CAPITAL LETTER O WITH INVERTED BREVE
+    { 0x0210, 0x0211 },  // LATIN CAPITAL LETTER R WITH DOUBLE GRAVE
+    { 0x0212, 0x0213 },  // LATIN CAPITAL LETTER R WITH INVERTED BREVE
+    { 0x0214, 0x0215 },  // LATIN CAPITAL LETTER U WITH DOUBLE GRAVE
+    { 0x0216, 0x0217 },  // LATIN CAPITAL LETTER U WITH INVERTED BREVE
+    { 0x0218, 0x0219 },  // LATIN CAPITAL LETTER S WITH COMMA BELOW
+    { 0x021A, 0x021B },  // LATIN CAPITAL LETTER T WITH COMMA BELOW
     { 0x021C, 0x021D },  // LATIN CAPITAL LETTER YOGH
+    { 0x021E, 0x021F },  // LATIN CAPITAL LETTER H WITH CARON
     { 0x0220, 0x019E },  // LATIN CAPITAL LETTER N WITH LONG RIGHT LEG
     { 0x0222, 0x0223 },  // LATIN CAPITAL LETTER OU
     { 0x0224, 0x0225 },  // LATIN CAPITAL LETTER Z WITH HOOK
+    { 0x0226, 0x0227 },  // LATIN CAPITAL LETTER A WITH DOT ABOVE
+    { 0x0228, 0x0229 },  // LATIN CAPITAL LETTER E WITH CEDILLA
+    { 0x022A, 0x022B },  // LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON
+    { 0x022C, 0x022D },  // LATIN CAPITAL LETTER O WITH TILDE AND MACRON
+    { 0x022E, 0x022F },  // LATIN CAPITAL LETTER O WITH DOT ABOVE
+    { 0x0230, 0x0231 },  // LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON
+    { 0x0232, 0x0233 },  // LATIN CAPITAL LETTER Y WITH MACRON
     { 0x023A, 0x2C65 },  // LATIN CAPITAL LETTER A WITH STROKE
     { 0x023B, 0x023C },  // LATIN CAPITAL LETTER C WITH STROKE
     { 0x023D, 0x019A },  // LATIN CAPITAL LETTER L WITH BAR
@@ -142,6 +278,13 @@
     { 0x0370, 0x0371 },  // GREEK CAPITAL LETTER HETA
     { 0x0372, 0x0373 },  // GREEK CAPITAL LETTER ARCHAIC SAMPI
     { 0x0376, 0x0377 },  // GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA
+    { 0x0386, 0x03AC },  // GREEK CAPITAL LETTER ALPHA WITH TONOS
+    { 0x0388, 0x03AD },  // GREEK CAPITAL LETTER EPSILON WITH TONOS
+    { 0x0389, 0x03AE },  // GREEK CAPITAL LETTER ETA WITH TONOS
+    { 0x038A, 0x03AF },  // GREEK CAPITAL LETTER IOTA WITH TONOS
+    { 0x038C, 0x03CC },  // GREEK CAPITAL LETTER OMICRON WITH TONOS
+    { 0x038E, 0x03CD },  // GREEK CAPITAL LETTER UPSILON WITH TONOS
+    { 0x038F, 0x03CE },  // GREEK CAPITAL LETTER OMEGA WITH TONOS
     { 0x0391, 0x03B1 },  // GREEK CAPITAL LETTER ALPHA
     { 0x0392, 0x03B2 },  // GREEK CAPITAL LETTER BETA
     { 0x0393, 0x03B3 },  // GREEK CAPITAL LETTER GAMMA
@@ -166,6 +309,8 @@
     { 0x03A7, 0x03C7 },  // GREEK CAPITAL LETTER CHI
     { 0x03A8, 0x03C8 },  // GREEK CAPITAL LETTER PSI
     { 0x03A9, 0x03C9 },  // GREEK CAPITAL LETTER OMEGA
+    { 0x03AA, 0x03CA },  // GREEK CAPITAL LETTER IOTA WITH DIALYTIKA
+    { 0x03AB, 0x03CB },  // GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA
     { 0x03CF, 0x03D7 },  // GREEK CAPITAL KAI SYMBOL
     { 0x03D8, 0x03D9 },  // GREEK LETTER ARCHAIC KOPPA
     { 0x03DA, 0x03DB },  // GREEK LETTER STIGMA
@@ -179,19 +324,28 @@
     { 0x03EA, 0x03EB },  // COPTIC CAPITAL LETTER GANGIA
     { 0x03EC, 0x03ED },  // COPTIC CAPITAL LETTER SHIMA
     { 0x03EE, 0x03EF },  // COPTIC CAPITAL LETTER DEI
+    { 0x03F4, 0x03B8 },  // GREEK CAPITAL THETA SYMBOL
     { 0x03F7, 0x03F8 },  // GREEK CAPITAL LETTER SHO
+    { 0x03F9, 0x03F2 },  // GREEK CAPITAL LUNATE SIGMA SYMBOL
     { 0x03FA, 0x03FB },  // GREEK CAPITAL LETTER SAN
     { 0x03FD, 0x037B },  // GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL
     { 0x03FE, 0x037C },  // GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL
     { 0x03FF, 0x037D },  // GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL
+    { 0x0400, 0x0450 },  // CYRILLIC CAPITAL LETTER IE WITH GRAVE
+    { 0x0401, 0x0451 },  // CYRILLIC CAPITAL LETTER IO
     { 0x0402, 0x0452 },  // CYRILLIC CAPITAL LETTER DJE
+    { 0x0403, 0x0453 },  // CYRILLIC CAPITAL LETTER GJE
     { 0x0404, 0x0454 },  // CYRILLIC CAPITAL LETTER UKRAINIAN IE
     { 0x0405, 0x0455 },  // CYRILLIC CAPITAL LETTER DZE
     { 0x0406, 0x0456 },  // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I
+    { 0x0407, 0x0457 },  // CYRILLIC CAPITAL LETTER YI
     { 0x0408, 0x0458 },  // CYRILLIC CAPITAL LETTER JE
     { 0x0409, 0x0459 },  // CYRILLIC CAPITAL LETTER LJE
     { 0x040A, 0x045A },  // CYRILLIC CAPITAL LETTER NJE
     { 0x040B, 0x045B },  // CYRILLIC CAPITAL LETTER TSHE
+    { 0x040C, 0x045C },  // CYRILLIC CAPITAL LETTER KJE
+    { 0x040D, 0x045D },  // CYRILLIC CAPITAL LETTER I WITH GRAVE
+    { 0x040E, 0x045E },  // CYRILLIC CAPITAL LETTER SHORT U
     { 0x040F, 0x045F },  // CYRILLIC CAPITAL LETTER DZHE
     { 0x0410, 0x0430 },  // CYRILLIC CAPITAL LETTER A
     { 0x0411, 0x0431 },  // CYRILLIC CAPITAL LETTER BE
@@ -236,6 +390,7 @@
     { 0x0470, 0x0471 },  // CYRILLIC CAPITAL LETTER PSI
     { 0x0472, 0x0473 },  // CYRILLIC CAPITAL LETTER FITA
     { 0x0474, 0x0475 },  // CYRILLIC CAPITAL LETTER IZHITSA
+    { 0x0476, 0x0477 },  // CYRILLIC CAPITAL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT
     { 0x0478, 0x0479 },  // CYRILLIC CAPITAL LETTER UK
     { 0x047A, 0x047B },  // CYRILLIC CAPITAL LETTER ROUND OMEGA
     { 0x047C, 0x047D },  // CYRILLIC CAPITAL LETTER OMEGA WITH TITLO
@@ -269,17 +424,34 @@
     { 0x04BC, 0x04BD },  // CYRILLIC CAPITAL LETTER ABKHASIAN CHE
     { 0x04BE, 0x04BF },  // CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER
     { 0x04C0, 0x04CF },  // CYRILLIC LETTER PALOCHKA
+    { 0x04C1, 0x04C2 },  // CYRILLIC CAPITAL LETTER ZHE WITH BREVE
     { 0x04C3, 0x04C4 },  // CYRILLIC CAPITAL LETTER KA WITH HOOK
     { 0x04C5, 0x04C6 },  // CYRILLIC CAPITAL LETTER EL WITH TAIL
     { 0x04C7, 0x04C8 },  // CYRILLIC CAPITAL LETTER EN WITH HOOK
     { 0x04C9, 0x04CA },  // CYRILLIC CAPITAL LETTER EN WITH TAIL
     { 0x04CB, 0x04CC },  // CYRILLIC CAPITAL LETTER KHAKASSIAN CHE
     { 0x04CD, 0x04CE },  // CYRILLIC CAPITAL LETTER EM WITH TAIL
+    { 0x04D0, 0x04D1 },  // CYRILLIC CAPITAL LETTER A WITH BREVE
+    { 0x04D2, 0x04D3 },  // CYRILLIC CAPITAL LETTER A WITH DIAERESIS
     { 0x04D4, 0x04D5 },  // CYRILLIC CAPITAL LIGATURE A IE
+    { 0x04D6, 0x04D7 },  // CYRILLIC CAPITAL LETTER IE WITH BREVE
     { 0x04D8, 0x04D9 },  // CYRILLIC CAPITAL LETTER SCHWA
+    { 0x04DA, 0x04DB },  // CYRILLIC CAPITAL LETTER SCHWA WITH DIAERESIS
+    { 0x04DC, 0x04DD },  // CYRILLIC CAPITAL LETTER ZHE WITH DIAERESIS
+    { 0x04DE, 0x04DF },  // CYRILLIC CAPITAL LETTER ZE WITH DIAERESIS
     { 0x04E0, 0x04E1 },  // CYRILLIC CAPITAL LETTER ABKHASIAN DZE
+    { 0x04E2, 0x04E3 },  // CYRILLIC CAPITAL LETTER I WITH MACRON
+    { 0x04E4, 0x04E5 },  // CYRILLIC CAPITAL LETTER I WITH DIAERESIS
+    { 0x04E6, 0x04E7 },  // CYRILLIC CAPITAL LETTER O WITH DIAERESIS
     { 0x04E8, 0x04E9 },  // CYRILLIC CAPITAL LETTER BARRED O
+    { 0x04EA, 0x04EB },  // CYRILLIC CAPITAL LETTER BARRED O WITH DIAERESIS
+    { 0x04EC, 0x04ED },  // CYRILLIC CAPITAL LETTER E WITH DIAERESIS
+    { 0x04EE, 0x04EF },  // CYRILLIC CAPITAL LETTER U WITH MACRON
+    { 0x04F0, 0x04F1 },  // CYRILLIC CAPITAL LETTER U WITH DIAERESIS
+    { 0x04F2, 0x04F3 },  // CYRILLIC CAPITAL LETTER U WITH DOUBLE ACUTE
+    { 0x04F4, 0x04F5 },  // CYRILLIC CAPITAL LETTER CHE WITH DIAERESIS
     { 0x04F6, 0x04F7 },  // CYRILLIC CAPITAL LETTER GHE WITH DESCENDER
+    { 0x04F8, 0x04F9 },  // CYRILLIC CAPITAL LETTER YERU WITH DIAERESIS
     { 0x04FA, 0x04FB },  // CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK
     { 0x04FC, 0x04FD },  // CYRILLIC CAPITAL LETTER HA WITH HOOK
     { 0x04FE, 0x04FF },  // CYRILLIC CAPITAL LETTER HA WITH STROKE
diff --git a/native/jni/src/char_utils.h b/native/jni/src/char_utils.h
index 58d388d..b429f40 100644
--- a/native/jni/src/char_utils.h
+++ b/native/jni/src/char_utils.h
@@ -58,7 +58,8 @@
 AK_FORCE_INLINE static int toLowerCase(const int c) {
     if (isAsciiUpper(c)) {
         return toAsciiLower(c);
-    } else if (isAscii(c)) {
+    }
+    if (isAscii(c)) {
         return c;
     }
     return static_cast<int>(latin_tolower(static_cast<unsigned short>(c)));
diff --git a/native/jni/src/correction.cpp b/native/jni/src/correction.cpp
index 76234f8..0c65939 100644
--- a/native/jni/src/correction.cpp
+++ b/native/jni/src/correction.cpp
@@ -675,7 +675,7 @@
                 multiplyIntCapped(typedLetterMultiplier, &finalFreq);
             }
             const float factor =
-                    SuggestUtils::getDistanceScalingFactor(static_cast<float>(squaredDistance));
+                    SuggestUtils::getLengthScalingFactor(static_cast<float>(squaredDistance));
             if (factor > 0.0f) {
                 multiplyRate(static_cast<int>(factor * 100.0f), &finalFreq);
             } else if (squaredDistance == PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO) {
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index a456912..6ef9f41 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -216,6 +216,7 @@
 #define DEBUG_DOUBLE_LETTER false
 #define DEBUG_CACHE false
 #define DEBUG_DUMP_ERROR false
+#define DEBUG_EVALUATE_MOST_PROBABLE_STRING false
 
 #ifdef FLAG_FULL_DBG
 #define DEBUG_GEO_FULL true
@@ -241,6 +242,7 @@
 #define DEBUG_DOUBLE_LETTER false
 #define DEBUG_CACHE false
 #define DEBUG_DUMP_ERROR false
+#define DEBUG_EVALUATE_MOST_PROBABLE_STRING false
 
 #define DEBUG_GEO_FULL false
 
@@ -422,10 +424,9 @@
     CT_OMISSION,
     CT_INSERTION,
     CT_TRANSPOSITION,
-    CT_SPACE_SUBSTITUTION,
-    CT_SPACE_OMISSION,
     CT_COMPLETION,
     CT_TERMINAL,
-    CT_NEW_WORD,
+    CT_NEW_WORD_SPACE_OMITTION,
+    CT_NEW_WORD_SPACE_SUBSTITUTION,
 } CorrectionType;
 #endif // LATINIME_DEFINES_H
diff --git a/native/jni/src/dictionary.cpp b/native/jni/src/dictionary.cpp
index ed6ddb5..c998c06 100644
--- a/native/jni/src/dictionary.cpp
+++ b/native/jni/src/dictionary.cpp
@@ -103,4 +103,9 @@
 bool Dictionary::isValidBigram(const int *word1, int length1, const int *word2, int length2) const {
     return mBigramDictionary->isValidBigram(word1, length1, word2, length2);
 }
+
+int Dictionary::getDictFlags() const {
+    return mUnigramDictionary->getDictFlags();
+}
+
 } // namespace latinime
diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h
index 8c6a7de..0653d3c 100644
--- a/native/jni/src/dictionary.h
+++ b/native/jni/src/dictionary.h
@@ -63,6 +63,7 @@
     int getDictSize() const { return mDictSize; }
     int getMmapFd() const { return mMmapFd; }
     int getDictBufAdjust() const { return mDictBufAdjust; }
+    int getDictFlags() const;
     virtual ~Dictionary();
 
  private:
diff --git a/native/jni/src/digraph_utils.cpp b/native/jni/src/digraph_utils.cpp
index 8781c50..0834426 100644
--- a/native/jni/src/digraph_utils.cpp
+++ b/native/jni/src/digraph_utils.cpp
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#include "char_utils.h"
 #include "binary_format.h"
 #include "defines.h"
 #include "digraph_utils.h"
@@ -27,39 +28,47 @@
 const DigraphUtils::digraph_t DigraphUtils::FRENCH_LIGATURES_DIGRAPHS[] =
         { { 'a', 'e', 0x00E6 }, // U+00E6 : LATIN SMALL LETTER AE
         { 'o', 'e', 0x0153 } }; // U+0153 : LATIN SMALL LIGATURE OE
+const DigraphUtils::DigraphType DigraphUtils::USED_DIGRAPH_TYPES[] =
+        { DIGRAPH_TYPE_GERMAN_UMLAUT, DIGRAPH_TYPE_FRENCH_LIGATURES };
 
 /* static */ bool DigraphUtils::hasDigraphForCodePoint(
         const int dictFlags, const int compositeGlyphCodePoint) {
-    if (DigraphUtils::getDigraphForCodePoint(dictFlags, compositeGlyphCodePoint)) {
+    const DigraphUtils::DigraphType digraphType = getDigraphTypeForDictionary(dictFlags);
+    if (DigraphUtils::getDigraphForDigraphTypeAndCodePoint(digraphType, compositeGlyphCodePoint)) {
         return true;
     }
     return false;
 }
 
-// Retrieves the set of all digraphs associated with the given dictionary.
-// Returns the size of the digraph array, or 0 if none exist.
-/* static */ int DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(
-        const int dictFlags, const DigraphUtils::digraph_t **digraphs) {
+// Returns the digraph type associated with the given dictionary.
+/* static */ DigraphUtils::DigraphType DigraphUtils::getDigraphTypeForDictionary(
+        const int dictFlags) {
     if (BinaryFormat::REQUIRES_GERMAN_UMLAUT_PROCESSING & dictFlags) {
-        *digraphs = DigraphUtils::GERMAN_UMLAUT_DIGRAPHS;
-        return NELEMS(DigraphUtils::GERMAN_UMLAUT_DIGRAPHS);
+        return DIGRAPH_TYPE_GERMAN_UMLAUT;
     }
     if (BinaryFormat::REQUIRES_FRENCH_LIGATURES_PROCESSING & dictFlags) {
-        *digraphs = DigraphUtils::FRENCH_LIGATURES_DIGRAPHS;
-        return NELEMS(DigraphUtils::FRENCH_LIGATURES_DIGRAPHS);
+        return DIGRAPH_TYPE_FRENCH_LIGATURES;
     }
-    return 0;
+    return DIGRAPH_TYPE_NONE;
+}
+
+// Retrieves the set of all digraphs associated with the given dictionary flags.
+// Returns the size of the digraph array, or 0 if none exist.
+/* static */ int DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(
+        const int dictFlags, const DigraphUtils::digraph_t **const digraphs) {
+    const DigraphUtils::DigraphType digraphType = getDigraphTypeForDictionary(dictFlags);
+    return getAllDigraphsForDigraphTypeAndReturnSize(digraphType, digraphs);
 }
 
 // Returns the digraph codepoint for the given composite glyph codepoint and digraph codepoint index
 // (which specifies the first or second codepoint in the digraph).
-/* static */ int DigraphUtils::getDigraphCodePointForIndex(const int dictFlags,
-        const int compositeGlyphCodePoint, const DigraphCodePointIndex digraphCodePointIndex) {
+/* static */ int DigraphUtils::getDigraphCodePointForIndex(const int compositeGlyphCodePoint,
+        const DigraphCodePointIndex digraphCodePointIndex) {
     if (digraphCodePointIndex == NOT_A_DIGRAPH_INDEX) {
         return NOT_A_CODE_POINT;
     }
-    const DigraphUtils::digraph_t *digraph =
-            DigraphUtils::getDigraphForCodePoint(dictFlags, compositeGlyphCodePoint);
+    const DigraphUtils::digraph_t *const digraph =
+            DigraphUtils::getDigraphForCodePoint(compositeGlyphCodePoint);
     if (!digraph) {
         return NOT_A_CODE_POINT;
     }
@@ -72,18 +81,51 @@
     return NOT_A_CODE_POINT;
 }
 
+// Retrieves the set of all digraphs associated with the given digraph type.
+// Returns the size of the digraph array, or 0 if none exist.
+/* static */ int DigraphUtils::getAllDigraphsForDigraphTypeAndReturnSize(
+        const DigraphUtils::DigraphType digraphType,
+        const DigraphUtils::digraph_t **const digraphs) {
+    if (digraphType == DigraphUtils::DIGRAPH_TYPE_GERMAN_UMLAUT) {
+        *digraphs = GERMAN_UMLAUT_DIGRAPHS;
+        return NELEMS(GERMAN_UMLAUT_DIGRAPHS);
+    }
+    if (digraphType == DIGRAPH_TYPE_FRENCH_LIGATURES) {
+        *digraphs = FRENCH_LIGATURES_DIGRAPHS;
+        return NELEMS(FRENCH_LIGATURES_DIGRAPHS);
+    }
+    return 0;
+}
+
 /**
  * Returns the digraph for the input composite glyph codepoint, or 0 if none exists.
- * dictFlags: the dictionary flags needed to determine which digraphs are supported.
  * compositeGlyphCodePoint: the method returns the digraph corresponding to this codepoint.
  */
 /* static */ const DigraphUtils::digraph_t *DigraphUtils::getDigraphForCodePoint(
-        const int dictFlags, const int compositeGlyphCodePoint) {
+        const int compositeGlyphCodePoint) {
+    for (size_t i = 0; i < NELEMS(USED_DIGRAPH_TYPES); i++) {
+        const DigraphUtils::digraph_t *const digraph = getDigraphForDigraphTypeAndCodePoint(
+                USED_DIGRAPH_TYPES[i], compositeGlyphCodePoint);
+        if (digraph) {
+            return digraph;
+        }
+    }
+    return 0;
+}
+
+/**
+ * Returns the digraph for the input composite glyph codepoint, or 0 if none exists.
+ * digraphType: the type of digraphs supported.
+ * compositeGlyphCodePoint: the method returns the digraph corresponding to this codepoint.
+ */
+/* static */ const DigraphUtils::digraph_t *DigraphUtils::getDigraphForDigraphTypeAndCodePoint(
+        const DigraphUtils::DigraphType digraphType, const int compositeGlyphCodePoint) {
     const DigraphUtils::digraph_t *digraphs = 0;
+    const int compositeGlyphLowerCodePoint = toLowerCase(compositeGlyphCodePoint);
     const int digraphsSize =
-            DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(dictFlags, &digraphs);
+            DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(digraphType, &digraphs);
     for (int i = 0; i < digraphsSize; i++) {
-        if (digraphs[i].compositeGlyph == compositeGlyphCodePoint) {
+        if (digraphs[i].compositeGlyph == compositeGlyphLowerCodePoint) {
             return &digraphs[i];
         }
     }
diff --git a/native/jni/src/digraph_utils.h b/native/jni/src/digraph_utils.h
index 6e364b6..9443522 100644
--- a/native/jni/src/digraph_utils.h
+++ b/native/jni/src/digraph_utils.h
@@ -27,21 +27,34 @@
         SECOND_DIGRAPH_CODEPOINT
     } DigraphCodePointIndex;
 
+    typedef enum {
+        DIGRAPH_TYPE_NONE,
+        DIGRAPH_TYPE_GERMAN_UMLAUT,
+        DIGRAPH_TYPE_FRENCH_LIGATURES
+    } DigraphType;
+
     typedef struct { int first; int second; int compositeGlyph; } digraph_t;
 
     static bool hasDigraphForCodePoint(const int dictFlags, const int compositeGlyphCodePoint);
     static int getAllDigraphsForDictionaryAndReturnSize(
-            const int dictFlags, const digraph_t **digraphs);
+            const int dictFlags, const digraph_t **const digraphs);
     static int getDigraphCodePointForIndex(const int dictFlags, const int compositeGlyphCodePoint,
             const DigraphCodePointIndex digraphCodePointIndex);
+    static int getDigraphCodePointForIndex(const int compositeGlyphCodePoint,
+            const DigraphCodePointIndex digraphCodePointIndex);
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DigraphUtils);
-    static const digraph_t *getDigraphForCodePoint(
-            const int dictFlags, const int compositeGlyphCodePoint);
+    static DigraphType getDigraphTypeForDictionary(const int dictFlags);
+    static int getAllDigraphsForDigraphTypeAndReturnSize(
+            const DigraphType digraphType, const digraph_t **const digraphs);
+    static const digraph_t *getDigraphForCodePoint(const int compositeGlyphCodePoint);
+    static const digraph_t *getDigraphForDigraphTypeAndCodePoint(
+            const DigraphType digraphType, const int compositeGlyphCodePoint);
 
     static const digraph_t GERMAN_UMLAUT_DIGRAPHS[];
     static const digraph_t FRENCH_LIGATURES_DIGRAPHS[];
+    static const DigraphType USED_DIGRAPH_TYPES[];
 };
 } // namespace latinime
 #endif // DIGRAPH_UTILS_H
diff --git a/native/jni/src/proximity_info.cpp b/native/jni/src/proximity_info.cpp
index 50f38e8..88d670d 100644
--- a/native/jni/src/proximity_info.cpp
+++ b/native/jni/src/proximity_info.cpp
@@ -49,13 +49,17 @@
 
 ProximityInfo::ProximityInfo(JNIEnv *env, const jstring localeJStr,
         const int keyboardWidth, const int keyboardHeight, const int gridWidth,
-        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)
+        const int gridHeight, const int mostCommonKeyWidth, const int mostCommonKeyHeight,
+        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)
         : GRID_WIDTH(gridWidth), GRID_HEIGHT(gridHeight), MOST_COMMON_KEY_WIDTH(mostCommonKeyWidth),
           MOST_COMMON_KEY_WIDTH_SQUARE(mostCommonKeyWidth * mostCommonKeyWidth),
+          MOST_COMMON_KEY_HEIGHT(mostCommonKeyHeight),
+          NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE(1.0f +
+                  SQUARE_FLOAT(static_cast<float>(mostCommonKeyHeight) /
+                          static_cast<float>(mostCommonKeyWidth))),
           CELL_WIDTH((keyboardWidth + gridWidth - 1) / gridWidth),
           CELL_HEIGHT((keyboardHeight + gridHeight - 1) / gridHeight),
           KEY_COUNT(min(keyCount, MAX_KEY_COUNT_IN_A_KEYBOARD)),
diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h
index e21262f..deb9ae0 100644
--- a/native/jni/src/proximity_info.h
+++ b/native/jni/src/proximity_info.h
@@ -30,11 +30,11 @@
  public:
     ProximityInfo(JNIEnv *env, const jstring localeJStr,
             const int keyboardWidth, const int keyboardHeight, const int gridWidth,
-            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);
+            const int gridHeight, const int mostCommonKeyWidth, const int mostCommonKeyHeight,
+            const jintArray proximityChars, const int keyCount, const jintArray keyXCoordinates,
+            const jintArray keyYCoordinates, const jintArray keyWidths, const jintArray keyHeights,
+            const jintArray keyCharCodes, const jfloatArray sweetSpotCenterXs,
+            const jfloatArray sweetSpotCenterYs, const jfloatArray sweetSpotRadii);
     ~ProximityInfo();
     bool hasSpaceProximity(const int x, const int y) const;
     int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const;
@@ -56,6 +56,9 @@
     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; }
+    float getNormalizedSquaredMostCommonKeyHypotenuse() const {
+        return NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE;
+    }
     int getKeyCount() const { return KEY_COUNT; }
     int getCellHeight() const { return CELL_HEIGHT; }
     int getCellWidth() const { return CELL_WIDTH; }
@@ -99,6 +102,8 @@
     const int GRID_HEIGHT;
     const int MOST_COMMON_KEY_WIDTH;
     const int MOST_COMMON_KEY_WIDTH_SQUARE;
+    const int MOST_COMMON_KEY_HEIGHT;
+    const float NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE;
     const int CELL_WIDTH;
     const int CELL_HEIGHT;
     const int KEY_COUNT;
diff --git a/native/jni/src/proximity_info_state.cpp b/native/jni/src/proximity_info_state.cpp
index a10b260..cc5b736 100644
--- a/native/jni/src/proximity_info_state.cpp
+++ b/native/jni/src/proximity_info_state.cpp
@@ -81,7 +81,7 @@
         mSampledTimes.clear();
         mSampledInputIndice.clear();
         mSampledLengthCache.clear();
-        mSampledDistanceCache_G.clear();
+        mSampledNormalizedSquaredLengthCache.clear();
         mSampledNearKeySets.clear();
         mSampledSearchKeySets.clear();
         mSpeedRates.clear();
@@ -122,14 +122,15 @@
     if (mSampledInputSize > 0) {
         ProximityInfoStateUtils::initGeometricDistanceInfos(mProximityInfo, mSampledInputSize,
                 lastSavedInputSize, verticalSweetSpotScale, &mSampledInputXs, &mSampledInputYs,
-                &mSampledNearKeySets, &mSampledDistanceCache_G);
+                &mSampledNearKeySets, &mSampledNormalizedSquaredLengthCache);
         if (isGeometric) {
             // updates probabilities of skipping or mapping each key for all points.
             ProximityInfoStateUtils::updateAlignPointProbabilities(
                     mMaxPointToKeyLength, mProximityInfo->getMostCommonKeyWidth(),
                     mProximityInfo->getKeyCount(), lastSavedInputSize, mSampledInputSize,
                     &mSampledInputXs, &mSampledInputYs, &mSpeedRates, &mSampledLengthCache,
-                    &mSampledDistanceCache_G, &mSampledNearKeySets, &mCharProbabilities);
+                    &mSampledNormalizedSquaredLengthCache, &mSampledNearKeySets,
+                    &mCharProbabilities);
             ProximityInfoStateUtils::updateSampledSearchKeySets(mProximityInfo,
                     mSampledInputSize, lastSavedInputSize, &mSampledLengthCache,
                     &mSampledNearKeySets, &mSampledSearchKeySets,
@@ -171,7 +172,7 @@
     const int keyId = mProximityInfo->getKeyIndexOf(codePoint);
     if (keyId != NOT_AN_INDEX) {
         const int index = inputIndex * mProximityInfo->getKeyCount() + keyId;
-        return min(mSampledDistanceCache_G[index], mMaxPointToKeyLength);
+        return min(mSampledNormalizedSquaredLengthCache[index], mMaxPointToKeyLength);
     }
     if (isIntentionalOmissionCodePoint(codePoint)) {
         return 0.0f;
@@ -183,7 +184,8 @@
 float ProximityInfoState::getPointToKeyByIdLength(
         const int inputIndex, const int keyId) const {
     return ProximityInfoStateUtils::getPointToKeyByIdLength(mMaxPointToKeyLength,
-            &mSampledDistanceCache_G, mProximityInfo->getKeyCount(), inputIndex, keyId);
+            &mSampledNormalizedSquaredLengthCache, mProximityInfo->getKeyCount(), inputIndex,
+            keyId);
 }
 
 // In the following function, c is the current character of the dictionary word currently examined.
diff --git a/native/jni/src/proximity_info_state.h b/native/jni/src/proximity_info_state.h
index 9bba751..bbe8af2 100644
--- a/native/jni/src/proximity_info_state.h
+++ b/native/jni/src/proximity_info_state.h
@@ -49,8 +49,8 @@
               mKeyCount(0), mCellHeight(0), mCellWidth(0), mGridHeight(0), mGridWidth(0),
               mIsContinuousSuggestionPossible(false), mSampledInputXs(), mSampledInputYs(),
               mSampledTimes(), mSampledInputIndice(), mSampledLengthCache(),
-              mBeelineSpeedPercentiles(), mSampledDistanceCache_G(), mSpeedRates(), mDirections(),
-              mCharProbabilities(), mSampledNearKeySets(), mSampledSearchKeySets(),
+              mBeelineSpeedPercentiles(), mSampledNormalizedSquaredLengthCache(), mSpeedRates(),
+              mDirections(), mCharProbabilities(), mSampledNearKeySets(), mSampledSearchKeySets(),
               mSampledSearchKeyVectors(), mTouchPositionCorrectionEnabled(false),
               mSampledInputSize(0), mMostProbableStringProbability(0.0f) {
         memset(mInputProximities, 0, sizeof(mInputProximities));
@@ -147,7 +147,9 @@
         return mIsContinuousSuggestionPossible;
     }
 
+    // TODO: Rename s/Length/NormalizedSquaredLength/
     float getPointToKeyByIdLength(const int inputIndex, const int keyId) const;
+    // TODO: Rename s/Length/NormalizedSquaredLength/
     float getPointToKeyLength(const int inputIndex, const int codePoint) const;
 
     ProximityType getProximityType(const int index, const int codePoint,
@@ -231,7 +233,7 @@
     std::vector<int> mSampledInputIndice;
     std::vector<int> mSampledLengthCache;
     std::vector<int> mBeelineSpeedPercentiles;
-    std::vector<float> mSampledDistanceCache_G;
+    std::vector<float> mSampledNormalizedSquaredLengthCache;
     std::vector<float> mSpeedRates;
     std::vector<float> mDirections;
     // probabilities of skipping or mapping to a key for each point.
diff --git a/native/jni/src/proximity_info_state_utils.cpp b/native/jni/src/proximity_info_state_utils.cpp
index df70cff..359673c 100644
--- a/native/jni/src/proximity_info_state_utils.cpp
+++ b/native/jni/src/proximity_info_state_utils.cpp
@@ -225,13 +225,13 @@
         const int lastSavedInputSize, const float verticalSweetSpotScale,
         const std::vector<int> *const sampledInputXs,
         const std::vector<int> *const sampledInputYs,
-        std::vector<NearKeycodesSet> *SampledNearKeySets,
-        std::vector<float> *SampledDistanceCache_G) {
-    SampledNearKeySets->resize(sampledInputSize);
+        std::vector<NearKeycodesSet> *sampledNearKeySets,
+        std::vector<float> *sampledNormalizedSquaredLengthCache) {
+    sampledNearKeySets->resize(sampledInputSize);
     const int keyCount = proximityInfo->getKeyCount();
-    SampledDistanceCache_G->resize(sampledInputSize * keyCount);
+    sampledNormalizedSquaredLengthCache->resize(sampledInputSize * keyCount);
     for (int i = lastSavedInputSize; i < sampledInputSize; ++i) {
-        (*SampledNearKeySets)[i].reset();
+        (*sampledNearKeySets)[i].reset();
         for (int k = 0; k < keyCount; ++k) {
             const int index = i * keyCount + k;
             const int x = (*sampledInputXs)[i];
@@ -239,10 +239,10 @@
             const float normalizedSquaredDistance =
                     proximityInfo->getNormalizedSquaredDistanceFromCenterFloatG(
                             k, x, y, verticalSweetSpotScale);
-            (*SampledDistanceCache_G)[index] = normalizedSquaredDistance;
+            (*sampledNormalizedSquaredLengthCache)[index] = normalizedSquaredDistance;
             if (normalizedSquaredDistance
                     < ProximityInfoParams::NEAR_KEY_NORMALIZED_SQUARED_THRESHOLD) {
-                (*SampledNearKeySets)[i][k] = true;
+                (*sampledNearKeySets)[i][k] = true;
             }
         }
     }
@@ -642,11 +642,11 @@
 // This function basically converts from a length to an edit distance. Accordingly, it's obviously
 // wrong to compare with mMaxPointToKeyLength.
 /* static */ float ProximityInfoStateUtils::getPointToKeyByIdLength(const float maxPointToKeyLength,
-        const std::vector<float> *const SampledDistanceCache_G, const int keyCount,
+        const std::vector<float> *const sampledNormalizedSquaredLengthCache, const int keyCount,
         const int inputIndex, const int keyId) {
     if (keyId != NOT_AN_INDEX) {
         const int index = inputIndex * keyCount + keyId;
-        return min((*SampledDistanceCache_G)[index], maxPointToKeyLength);
+        return min((*sampledNormalizedSquaredLengthCache)[index], maxPointToKeyLength);
     }
     // If the char is not a key on the keyboard then return the max length.
     return static_cast<float>(MAX_VALUE_FOR_WEIGHTING);
@@ -660,8 +660,8 @@
         const std::vector<int> *const sampledInputYs,
         const std::vector<float> *const sampledSpeedRates,
         const std::vector<int> *const sampledLengthCache,
-        const std::vector<float> *const SampledDistanceCache_G,
-        std::vector<NearKeycodesSet> *SampledNearKeySets,
+        const std::vector<float> *const sampledNormalizedSquaredLengthCache,
+        std::vector<NearKeycodesSet> *sampledNearKeySets,
         std::vector<hash_map_compat<int, float> > *charProbabilities) {
     charProbabilities->resize(sampledInputSize);
     // Calculates probabilities of using a point as a correlated point with the character
@@ -677,9 +677,9 @@
 
         float nearestKeyDistance = static_cast<float>(MAX_VALUE_FOR_WEIGHTING);
         for (int j = 0; j < keyCount; ++j) {
-            if ((*SampledNearKeySets)[i].test(j)) {
+            if ((*sampledNearKeySets)[i].test(j)) {
                 const float distance = getPointToKeyByIdLength(
-                        maxPointToKeyLength, SampledDistanceCache_G, keyCount, i, j);
+                        maxPointToKeyLength, sampledNormalizedSquaredLengthCache, keyCount, i, j);
                 if (distance < nearestKeyDistance) {
                     nearestKeyDistance = distance;
                 }
@@ -758,14 +758,15 @@
         // Summing up probability densities of all near keys.
         float sumOfProbabilityDensities = 0.0f;
         for (int j = 0; j < keyCount; ++j) {
-            if ((*SampledNearKeySets)[i].test(j)) {
+            if ((*sampledNearKeySets)[i].test(j)) {
                 float distance = sqrtf(getPointToKeyByIdLength(
-                        maxPointToKeyLength, SampledDistanceCache_G, keyCount, i, j));
+                        maxPointToKeyLength, sampledNormalizedSquaredLengthCache, keyCount, i, j));
                 if (i == 0 && i != sampledInputSize - 1) {
                     // For the first point, weighted average of distances from first point and the
                     // next point to the key is used as a point to key distance.
                     const float nextDistance = sqrtf(getPointToKeyByIdLength(
-                            maxPointToKeyLength, SampledDistanceCache_G, keyCount, i + 1, j));
+                            maxPointToKeyLength, sampledNormalizedSquaredLengthCache, keyCount,
+                            i + 1, j));
                     if (nextDistance < distance) {
                         // The distance of the first point tends to bigger than continuing
                         // points because the first touch by the user can be sloppy.
@@ -779,7 +780,8 @@
                     // For the first point, weighted average of distances from last point and
                     // the previous point to the key is used as a point to key distance.
                     const float previousDistance = sqrtf(getPointToKeyByIdLength(
-                            maxPointToKeyLength, SampledDistanceCache_G, keyCount, i - 1, j));
+                            maxPointToKeyLength, sampledNormalizedSquaredLengthCache, keyCount,
+                            i - 1, j));
                     if (previousDistance < distance) {
                         // The distance of the last point tends to bigger than continuing points
                         // because the last touch by the user can be sloppy. So we promote the
@@ -798,14 +800,15 @@
 
         // Split the probability of an input point to keys that are close to the input point.
         for (int j = 0; j < keyCount; ++j) {
-            if ((*SampledNearKeySets)[i].test(j)) {
+            if ((*sampledNearKeySets)[i].test(j)) {
                 float distance = sqrtf(getPointToKeyByIdLength(
-                        maxPointToKeyLength, SampledDistanceCache_G, keyCount, i, j));
+                        maxPointToKeyLength, sampledNormalizedSquaredLengthCache, keyCount, i, j));
                 if (i == 0 && i != sampledInputSize - 1) {
                     // For the first point, weighted average of distances from the first point and
                     // the next point to the key is used as a point to key distance.
                     const float prevDistance = sqrtf(getPointToKeyByIdLength(
-                            maxPointToKeyLength, SampledDistanceCache_G, keyCount, i + 1, j));
+                            maxPointToKeyLength, sampledNormalizedSquaredLengthCache, keyCount,
+                            i + 1, j));
                     if (prevDistance < distance) {
                         distance = (distance
                                 + prevDistance * ProximityInfoParams::NEXT_DISTANCE_WEIGHT)
@@ -815,7 +818,8 @@
                     // For the first point, weighted average of distances from last point and
                     // the previous point to the key is used as a point to key distance.
                     const float prevDistance = sqrtf(getPointToKeyByIdLength(
-                            maxPointToKeyLength, SampledDistanceCache_G, keyCount, i - 1, j));
+                            maxPointToKeyLength, sampledNormalizedSquaredLengthCache, keyCount,
+                            i - 1, j));
                     if (prevDistance < distance) {
                         distance = (distance
                                 + prevDistance * ProximityInfoParams::PREV_DISTANCE_WEIGHT)
@@ -882,10 +886,10 @@
         for (int j = 0; j < keyCount; ++j) {
             hash_map_compat<int, float>::iterator it = (*charProbabilities)[i].find(j);
             if (it == (*charProbabilities)[i].end()){
-                (*SampledNearKeySets)[i].reset(j);
+                (*sampledNearKeySets)[i].reset(j);
             } else if(it->second < ProximityInfoParams::MIN_PROBABILITY) {
                 // Erases from near keys vector because it has very low probability.
-                (*SampledNearKeySets)[i].reset(j);
+                (*sampledNearKeySets)[i].reset(j);
                 (*charProbabilities)[i].erase(j);
             } else {
                 it->second = -logf(it->second);
@@ -899,7 +903,7 @@
         const ProximityInfo *const proximityInfo, const int sampledInputSize,
         const int lastSavedInputSize,
         const std::vector<int> *const sampledLengthCache,
-        const std::vector<NearKeycodesSet> *const SampledNearKeySets,
+        const std::vector<NearKeycodesSet> *const sampledNearKeySets,
         std::vector<NearKeycodesSet> *sampledSearchKeySets,
         std::vector<std::vector<int> > *sampledSearchKeyVectors) {
     sampledSearchKeySets->resize(sampledInputSize);
@@ -916,7 +920,7 @@
             if ((*sampledLengthCache)[j] - (*sampledLengthCache)[i] >= readForwordLength) {
                 break;
             }
-            (*sampledSearchKeySets)[i] |= (*SampledNearKeySets)[j];
+            (*sampledSearchKeySets)[i] |= (*sampledNearKeySets)[j];
         }
     }
     const int keyCount = proximityInfo->getKeyCount();
diff --git a/native/jni/src/proximity_info_state_utils.h b/native/jni/src/proximity_info_state_utils.h
index c9feb59..1837c7a 100644
--- a/native/jni/src/proximity_info_state_utils.h
+++ b/native/jni/src/proximity_info_state_utils.h
@@ -71,25 +71,25 @@
             const std::vector<int> *const sampledInputYs,
             const std::vector<float> *const sampledSpeedRates,
             const std::vector<int> *const sampledLengthCache,
-            const std::vector<float> *const SampledDistanceCache_G,
-            std::vector<NearKeycodesSet> *SampledNearKeySets,
+            const std::vector<float> *const sampledNormalizedSquaredLengthCache,
+            std::vector<NearKeycodesSet> *sampledNearKeySets,
             std::vector<hash_map_compat<int, float> > *charProbabilities);
     static void updateSampledSearchKeySets(const ProximityInfo *const proximityInfo,
             const int sampledInputSize, const int lastSavedInputSize,
             const std::vector<int> *const sampledLengthCache,
-            const std::vector<NearKeycodesSet> *const SampledNearKeySets,
+            const std::vector<NearKeycodesSet> *const sampledNearKeySets,
             std::vector<NearKeycodesSet> *sampledSearchKeySets,
             std::vector<std::vector<int> > *sampledSearchKeyVectors);
     static float getPointToKeyByIdLength(const float maxPointToKeyLength,
-            const std::vector<float> *const SampledDistanceCache_G, const int keyCount,
+            const std::vector<float> *const sampledNormalizedSquaredLengthCache, const int keyCount,
             const int inputIndex, const int keyId);
     static void initGeometricDistanceInfos(const ProximityInfo *const proximityInfo,
             const int sampledInputSize, const int lastSavedInputSize,
             const float verticalSweetSpotScale,
             const std::vector<int> *const sampledInputXs,
             const std::vector<int> *const sampledInputYs,
-            std::vector<NearKeycodesSet> *SampledNearKeySets,
-            std::vector<float> *SampledDistanceCache_G);
+            std::vector<NearKeycodesSet> *sampledNearKeySets,
+            std::vector<float> *sampledNormalizedSquaredLengthCache);
     static void initPrimaryInputWord(const int inputSize, const int *const inputProximities,
             int *primaryInputWord);
     static void initNormalizedSquaredDistances(const ProximityInfo *const proximityInfo,
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index cde7b99..32faae5 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -23,6 +23,7 @@
 #include "dic_node_profiler.h"
 #include "dic_node_properties.h"
 #include "dic_node_release_listener.h"
+#include "digraph_utils.h"
 
 #if DEBUG_DICT
 #define LOGI_SHOW_ADD_COST_PROP \
@@ -399,8 +400,15 @@
     // TODO: Remove     //
     //////////////////////
     // TODO: Remove once touch path is merged into ProximityInfoState
+    // Note: Returned codepoint may be a digraph codepoint if the node is in a composite glyph.
     int getNodeCodePoint() const {
-        return mDicNodeProperties.getNodeCodePoint();
+        const int codePoint = mDicNodeProperties.getNodeCodePoint();
+        const DigraphUtils::DigraphCodePointIndex digraphIndex =
+                mDicNodeState.mDicNodeStateScoring.getDigraphIndex();
+        if (digraphIndex == DigraphUtils::NOT_A_DIGRAPH_INDEX) {
+            return codePoint;
+        }
+        return DigraphUtils::getDigraphCodePointForIndex(codePoint, digraphIndex);
     }
 
     ////////////////////////////////
@@ -452,6 +460,15 @@
         mDicNodeState.mDicNodeStateScoring.setDoubleLetterLevel(doubleLetterLevel);
     }
 
+    bool isInDigraph() const {
+        return mDicNodeState.mDicNodeStateScoring.getDigraphIndex()
+                != DigraphUtils::NOT_A_DIGRAPH_INDEX;
+    }
+
+    void advanceDigraphIndex() {
+        mDicNodeState.mDicNodeStateScoring.advanceDigraphIndex();
+    }
+
     uint8_t getFlags() const {
         return mDicNodeProperties.getFlags();
     }
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h b/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h
index 8e81632..8902d31 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h
@@ -20,6 +20,7 @@
 #include <stdint.h>
 
 #include "defines.h"
+#include "digraph_utils.h"
 
 namespace latinime {
 
@@ -27,6 +28,7 @@
  public:
     AK_FORCE_INLINE DicNodeStateScoring()
             : mDoubleLetterLevel(NOT_A_DOUBLE_LETTER),
+              mDigraphIndex(DigraphUtils::NOT_A_DIGRAPH_INDEX),
               mEditCorrectionCount(0), mProximityCorrectionCount(0),
               mNormalizedCompoundDistance(0.0f), mSpatialDistance(0.0f), mLanguageDistance(0.0f),
               mTotalPrevWordsLanguageCost(0.0f), mRawLength(0.0f) {
@@ -43,6 +45,7 @@
         mTotalPrevWordsLanguageCost = 0.0f;
         mRawLength = 0.0f;
         mDoubleLetterLevel = NOT_A_DOUBLE_LETTER;
+        mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX;
     }
 
     AK_FORCE_INLINE void init(const DicNodeStateScoring *const scoring) {
@@ -54,6 +57,7 @@
         mTotalPrevWordsLanguageCost = scoring->mTotalPrevWordsLanguageCost;
         mRawLength = scoring->mRawLength;
         mDoubleLetterLevel = scoring->mDoubleLetterLevel;
+        mDigraphIndex = scoring->mDigraphIndex;
     }
 
     void addCost(const float spatialCost, const float languageCost, const bool doNormalization,
@@ -126,6 +130,24 @@
         }
     }
 
+    DigraphUtils::DigraphCodePointIndex getDigraphIndex() const {
+        return mDigraphIndex;
+    }
+
+    void advanceDigraphIndex() {
+        switch(mDigraphIndex) {
+            case DigraphUtils::NOT_A_DIGRAPH_INDEX:
+                mDigraphIndex = DigraphUtils::FIRST_DIGRAPH_CODEPOINT;
+                break;
+            case DigraphUtils::FIRST_DIGRAPH_CODEPOINT:
+                mDigraphIndex = DigraphUtils::SECOND_DIGRAPH_CODEPOINT;
+                break;
+            case DigraphUtils::SECOND_DIGRAPH_CODEPOINT:
+                mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX;
+                break;
+        }
+    }
+
     float getTotalPrevWordsLanguageCost() const {
         return mTotalPrevWordsLanguageCost;
     }
@@ -135,6 +157,7 @@
     // Use a default copy constructor and an assign operator because shallow copies are ok
     // for this class
     DoubleLetterLevel mDoubleLetterLevel;
+    DigraphUtils::DigraphCodePointIndex mDigraphIndex;
 
     int16_t mEditCorrectionCount;
     int16_t mProximityCorrectionCount;
diff --git a/native/jni/src/suggest/core/policy/weighting.cpp b/native/jni/src/suggest/core/policy/weighting.cpp
index e62b704..b9c0b81 100644
--- a/native/jni/src/suggest/core/policy/weighting.cpp
+++ b/native/jni/src/suggest/core/policy/weighting.cpp
@@ -38,7 +38,7 @@
     case CT_SUBSTITUTION:
         PROF_SUBSTITUTION(node->mProfiler);
         return;
-    case CT_NEW_WORD:
+    case CT_NEW_WORD_SPACE_OMITTION:
         PROF_NEW_WORD(node->mProfiler);
         return;
     case CT_MATCH:
@@ -50,7 +50,7 @@
     case CT_TERMINAL:
         PROF_TERMINAL(node->mProfiler);
         return;
-    case CT_SPACE_SUBSTITUTION:
+    case CT_NEW_WORD_SPACE_SUBSTITUTION:
         PROF_SPACE_SUBSTITUTION(node->mProfiler);
         return;
     case CT_INSERTION:
@@ -107,16 +107,16 @@
     case CT_SUBSTITUTION:
         // only used for typing
         return weighting->getSubstitutionCost();
-    case CT_NEW_WORD:
-        return weighting->getNewWordCost(dicNode);
+    case CT_NEW_WORD_SPACE_OMITTION:
+        return weighting->getNewWordCost(traverseSession, dicNode);
     case CT_MATCH:
         return weighting->getMatchedCost(traverseSession, dicNode, inputStateG);
     case CT_COMPLETION:
         return weighting->getCompletionCost(traverseSession, dicNode);
     case CT_TERMINAL:
         return weighting->getTerminalSpatialCost(traverseSession, dicNode);
-    case CT_SPACE_SUBSTITUTION:
-        return weighting->getSpaceSubstitutionCost();
+    case CT_NEW_WORD_SPACE_SUBSTITUTION:
+        return weighting->getSpaceSubstitutionCost(traverseSession, dicNode);
     case CT_INSERTION:
         return weighting->getInsertionCost(traverseSession, parentDicNode, dicNode);
     case CT_TRANSPOSITION:
@@ -135,7 +135,7 @@
         return 0.0f;
     case CT_SUBSTITUTION:
         return 0.0f;
-    case CT_NEW_WORD:
+    case CT_NEW_WORD_SPACE_OMITTION:
         return weighting->getNewWordBigramCost(traverseSession, parentDicNode, bigramCacheMap);
     case CT_MATCH:
         return 0.0f;
@@ -147,8 +147,8 @@
                         traverseSession->getOffsetDict(), dicNode, bigramCacheMap);
         return weighting->getTerminalLanguageCost(traverseSession, dicNode, languageImprobability);
     }
-    case CT_SPACE_SUBSTITUTION:
-        return 0.0f;
+    case CT_NEW_WORD_SPACE_SUBSTITUTION:
+        return weighting->getNewWordBigramCost(traverseSession, parentDicNode, bigramCacheMap);
     case CT_INSERTION:
         return 0.0f;
     case CT_TRANSPOSITION:
@@ -168,7 +168,7 @@
         case CT_SUBSTITUTION:
             // Should return true?
             return false;
-        case CT_NEW_WORD:
+        case CT_NEW_WORD_SPACE_OMITTION:
             return false;
         case CT_MATCH:
             return false;
@@ -176,7 +176,7 @@
             return false;
         case CT_TERMINAL:
             return false;
-        case CT_SPACE_SUBSTITUTION:
+        case CT_NEW_WORD_SPACE_SUBSTITUTION:
             return false;
         case CT_INSERTION:
             return true;
@@ -197,7 +197,7 @@
             return false;
         case CT_SUBSTITUTION:
             return false;
-        case CT_NEW_WORD:
+        case CT_NEW_WORD_SPACE_OMITTION:
             return false;
         case CT_MATCH:
             return weighting->isProximityDicNode(traverseSession, dicNode);
@@ -205,7 +205,7 @@
             return false;
         case CT_TERMINAL:
             return false;
-        case CT_SPACE_SUBSTITUTION:
+        case CT_NEW_WORD_SPACE_SUBSTITUTION:
             return false;
         case CT_INSERTION:
             return false;
@@ -224,7 +224,7 @@
             return 0;
         case CT_SUBSTITUTION:
             return 0;
-        case CT_NEW_WORD:
+        case CT_NEW_WORD_SPACE_OMITTION:
             return 0;
         case CT_MATCH:
             return 1;
@@ -232,7 +232,7 @@
             return 0;
         case CT_TERMINAL:
             return 0;
-        case CT_SPACE_SUBSTITUTION:
+        case CT_NEW_WORD_SPACE_SUBSTITUTION:
             return 1;
         case CT_INSERTION:
             return 2;
diff --git a/native/jni/src/suggest/core/policy/weighting.h b/native/jni/src/suggest/core/policy/weighting.h
index b92dbe2..bce479c 100644
--- a/native/jni/src/suggest/core/policy/weighting.h
+++ b/native/jni/src/suggest/core/policy/weighting.h
@@ -56,7 +56,8 @@
             const DicTraverseSession *const traverseSession,
             const DicNode *const parentDicNode, const DicNode *const dicNode) const = 0;
 
-    virtual float getNewWordCost(const DicNode *const dicNode) const = 0;
+    virtual float getNewWordCost(const DicTraverseSession *const traverseSession,
+            const DicNode *const dicNode) const = 0;
 
     virtual float getNewWordBigramCost(
             const DicTraverseSession *const traverseSession, const DicNode *const dicNode,
@@ -76,7 +77,8 @@
 
     virtual float getSubstitutionCost() const = 0;
 
-    virtual float getSpaceSubstitutionCost() const = 0;
+    virtual float getSpaceSubstitutionCost(const DicTraverseSession *const traverseSession,
+            const DicNode *const dicNode) const = 0;
 
     Weighting() {}
     virtual ~Weighting() {}
diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.cpp b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
index ef6616e..3c44db2 100644
--- a/native/jni/src/suggest/core/session/dic_traverse_session.cpp
+++ b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
@@ -16,6 +16,7 @@
 
 #include "suggest/core/session/dic_traverse_session.h"
 
+#include "binary_format.h"
 #include "defines.h"
 #include "dictionary.h"
 #include "dic_traverse_wrapper.h"
@@ -63,6 +64,7 @@
 void DicTraverseSession::init(const Dictionary *const dictionary, const int *prevWord,
         int prevWordLength) {
     mDictionary = dictionary;
+    mMultiWordCostMultiplier = BinaryFormat::getMultiWordCostMultiplier(mDictionary->getDict());
     if (!prevWord) {
         mPrevWordPos = NOT_VALID_WORD;
         return;
@@ -84,6 +86,10 @@
     return mDictionary->getOffsetDict();
 }
 
+int DicTraverseSession::getDictFlags() const {
+    return mDictionary->getDictFlags();
+}
+
 void DicTraverseSession::resetCache(const int nextActiveCacheSize, const int maxWords) {
     mDicNodesCache.reset(nextActiveCacheSize, maxWords);
     mBigramCacheMap.clear();
diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.h b/native/jni/src/suggest/core/session/dic_traverse_session.h
index 62e1d1a..d9c2a51 100644
--- a/native/jni/src/suggest/core/session/dic_traverse_session.h
+++ b/native/jni/src/suggest/core/session/dic_traverse_session.h
@@ -36,7 +36,8 @@
     AK_FORCE_INLINE DicTraverseSession(JNIEnv *env, jstring localeStr)
             : mPrevWordPos(NOT_VALID_WORD), mProximityInfo(0),
               mDictionary(0), mDicNodesCache(), mBigramCacheMap(),
-              mInputSize(0), mPartiallyCommited(false), mMaxPointerCount(1) {
+              mInputSize(0), mPartiallyCommited(false), mMaxPointerCount(1),
+              mMultiWordCostMultiplier(1.0f) {
         // NOTE: mProximityInfoStates is an array of instances.
         // No need to initialize it explicitly here.
     }
@@ -52,8 +53,9 @@
             const int maxPointerCount);
     void resetCache(const int nextActiveCacheSize, const int maxWords);
 
+    // TODO: Remove
     const uint8_t *getOffsetDict() const;
-    bool canUseCache() const;
+    int getDictFlags() const;
 
     //--------------------
     // getters and setters
@@ -134,7 +136,7 @@
         if (!mDicNodesCache.hasCachedDicNodesForContinuousSuggestion()) {
             return false;
         }
-        ASSERT(mMaxPointerCount < MAX_POINTER_COUNT_G);
+        ASSERT(mMaxPointerCount <= MAX_POINTER_COUNT_G);
         for (int i = 0; i < mMaxPointerCount; ++i) {
             const ProximityInfoState *const pInfoState = getProximityInfoState(i);
             // If a proximity info state is not continuous suggestion possible,
@@ -146,6 +148,14 @@
         return true;
     }
 
+    bool isTouchPositionCorrectionEnabled() const {
+        return mProximityInfoStates[0].touchPositionCorrectionEnabled();
+    }
+
+    float getMultiWordCostMultiplier() const {
+        return mMultiWordCostMultiplier;
+    }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DicTraverseSession);
     // threshold to start caching
@@ -166,6 +176,11 @@
     int mInputSize;
     bool mPartiallyCommited;
     int mMaxPointerCount;
+
+    /////////////////////////////////
+    // Configuration per dictionary
+    float mMultiWordCostMultiplier;
+
 };
 } // namespace latinime
 #endif // LATINIME_DIC_TRAVERSE_SESSION_H
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index 764c372..9de2cd2 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -18,6 +18,7 @@
 
 #include "char_utils.h"
 #include "dictionary.h"
+#include "digraph_utils.h"
 #include "proximity_info.h"
 #include "suggest/core/dicnode/dic_node.h"
 #include "suggest/core/dicnode/dic_node_priority_queue.h"
@@ -32,16 +33,9 @@
 namespace latinime {
 
 // Initialization of class constants.
-const int Suggest::LOOKAHEAD_DIC_NODES_CACHE_SIZE = 25;
 const int Suggest::MIN_LEN_FOR_MULTI_WORD_AUTOCORRECT = 16;
 const int Suggest::MIN_CONTINUOUS_SUGGESTION_INPUT_SIZE = 2;
 const float Suggest::AUTOCORRECT_CLASSIFICATION_THRESHOLD = 0.33f;
-const float Suggest::AUTOCORRECT_LANGUAGE_FEATURE_THRESHOLD = 0.6f;
-
-const bool Suggest::CORRECT_SPACE_OMISSION = true;
-const bool Suggest::CORRECT_TRANSPOSITION = true;
-const bool Suggest::CORRECT_INSERTION = true;
-const bool Suggest::CORRECT_OMISSION_G = true;
 
 /**
  * Returns a set of suggestions for the given input touch points. The commitPoint argument indicates
@@ -123,8 +117,12 @@
  */
 int Suggest::outputSuggestions(DicTraverseSession *traverseSession, int *frequencies,
         int *outputCodePoints, int *spaceIndices, int *outputTypes) const {
+#if DEBUG_EVALUATE_MOST_PROBABLE_STRING
+    const int terminalSize = 0;
+#else
     const int terminalSize = min(MAX_RESULTS,
             static_cast<int>(traverseSession->getDicTraverseCache()->terminalSize()));
+#endif
     DicNode terminals[MAX_RESULTS]; // Avoiding non-POD variable length array
 
     for (int index = terminalSize - 1; index >= 0; --index) {
@@ -221,7 +219,7 @@
 void Suggest::expandCurrentDicNodes(DicTraverseSession *traverseSession) const {
     const int inputSize = traverseSession->getInputSize();
     DicNodeVector childDicNodes(TRAVERSAL->getDefaultExpandDicNodeSize());
-    DicNode omissionDicNode;
+    DicNode correctionDicNode;
 
     // TODO: Find more efficient caching
     const bool shouldDepthLevelCache = TRAVERSAL->shouldDepthLevelCache(traverseSession);
@@ -257,17 +255,16 @@
             dicNode.setCached();
         }
 
-        if (isLookAheadCorrection) {
+        if (dicNode.isInDigraph()) {
+            // Finish digraph handling if the node is in the middle of a digraph expansion.
+            processDicNodeAsDigraph(traverseSession, &dicNode);
+        } else if (isLookAheadCorrection) {
             // The algorithm maintains a small set of "deferred" nodes that have not consumed the
             // latest touch point yet. These are needed to apply look-ahead correction operations
             // that require special handling of the latest touch point. For example, with insertions
             // (e.g., "thiis" -> "this") the latest touch point should not be consumed at all.
-            if (CORRECT_TRANSPOSITION) {
-                processDicNodeAsTransposition(traverseSession, &dicNode);
-            }
-            if (CORRECT_INSERTION) {
-                processDicNodeAsInsertion(traverseSession, &dicNode);
-            }
+            processDicNodeAsTransposition(traverseSession, &dicNode);
+            processDicNodeAsInsertion(traverseSession, &dicNode);
         } else { // !isLookAheadCorrection
             // Only consider typing error corrections if the normalized compound distance is
             // below a spatial distance threshold.
@@ -291,12 +288,18 @@
                     processDicNodeAsMatch(traverseSession, childDicNode);
                     continue;
                 }
+                if (DigraphUtils::hasDigraphForCodePoint(traverseSession->getDictFlags(),
+                        childDicNode->getNodeCodePoint())) {
+                    correctionDicNode.initByCopy(childDicNode);
+                    correctionDicNode.advanceDigraphIndex();
+                    processDicNodeAsDigraph(traverseSession, &correctionDicNode);
+                }
                 if (allowsErrorCorrections
                         && TRAVERSAL->isOmission(traverseSession, &dicNode, childDicNode)) {
                     // TODO: (Gesture) Change weight between omission and substitution errors
                     // TODO: (Gesture) Terminal node should not be handled as omission
-                    omissionDicNode.initByCopy(childDicNode);
-                    processDicNodeAsOmission(traverseSession, &omissionDicNode);
+                    correctionDicNode.initByCopy(childDicNode);
+                    processDicNodeAsOmission(traverseSession, &correctionDicNode);
                 }
                 const ProximityType proximityType = TRAVERSAL->getProximityType(
                         traverseSession, &dicNode, childDicNode);
@@ -400,6 +403,16 @@
     processExpandedDicNode(traverseSession, childDicNode);
 }
 
+// Process the node codepoint as a digraph. This means that composite glyphs like the German
+// u-umlaut is expanded to the transliteration "ue". Note that this happens in parallel with
+// the normal non-digraph traversal, so both "uber" and "ueber" can be corrected to "[u-umlaut]ber".
+void Suggest::processDicNodeAsDigraph(DicTraverseSession *traverseSession,
+        DicNode *childDicNode) const {
+    weightChildNode(traverseSession, childDicNode);
+    childDicNode->advanceDigraphIndex();
+    processExpandedDicNode(traverseSession, childDicNode);
+}
+
 /**
  * Handle the dicNode as an omission error (e.g., ths => this). Skip the current letter and consider
  * matches for all possible next letters. Note that just skipping the current letter without any
@@ -507,13 +520,10 @@
     DicNode newDicNode;
     DicNodeUtils::initAsRootWithPreviousWord(traverseSession->getDicRootPos(),
             traverseSession->getOffsetDict(), dicNode, &newDicNode);
-    Weighting::addCostAndForwardInputIndex(WEIGHTING, CT_NEW_WORD, traverseSession, dicNode,
+    const CorrectionType correctionType = spaceSubstitution ?
+            CT_NEW_WORD_SPACE_SUBSTITUTION : CT_NEW_WORD_SPACE_OMITTION;
+    Weighting::addCostAndForwardInputIndex(WEIGHTING, correctionType, traverseSession, dicNode,
             &newDicNode, traverseSession->getBigramCacheMap());
-    if (spaceSubstitution) {
-        // Merge this with CT_NEW_WORD
-        Weighting::addCostAndForwardInputIndex(WEIGHTING, CT_SPACE_SUBSTITUTION,
-                traverseSession, 0, &newDicNode, 0 /* bigramCacheMap */);
-    }
     traverseSession->getDicTraverseCache()->copyPushNextActive(&newDicNode);
 }
 } // namespace latinime
diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h
index 6c09b94..875cbe4 100644
--- a/native/jni/src/suggest/core/suggest.h
+++ b/native/jni/src/suggest/core/suggest.h
@@ -42,8 +42,9 @@
 class Suggest : public SuggestInterface {
  public:
     AK_FORCE_INLINE Suggest(const SuggestPolicy *const suggestPolicy)
-            : TRAVERSAL(suggestPolicy->getTraversal()),
-              SCORING(suggestPolicy->getScoring()), WEIGHTING(suggestPolicy->getWeighting()) {}
+            : TRAVERSAL(suggestPolicy ? suggestPolicy->getTraversal() : 0),
+              SCORING(suggestPolicy ? suggestPolicy->getScoring() : 0),
+              WEIGHTING(suggestPolicy ? suggestPolicy->getWeighting() : 0) {}
     AK_FORCE_INLINE virtual ~Suggest() {}
     int getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs, int *inputYs,
             int *times, int *pointerIds, int *inputCodePoints, int inputSize, int commitPoint,
@@ -64,6 +65,7 @@
     void generateFeatures(
             DicTraverseSession *traverseSession, DicNode *dicNode, float *features) const;
     void processDicNodeAsOmission(DicTraverseSession *traverseSession, DicNode *dicNode) const;
+    void processDicNodeAsDigraph(DicTraverseSession *traverseSession, DicNode *dicNode) const;
     void processDicNodeAsTransposition(DicTraverseSession *traverseSession,
             DicNode *dicNode) const;
     void processDicNodeAsInsertion(DicTraverseSession *traverseSession, DicNode *dicNode) const;
@@ -74,31 +76,16 @@
     void processDicNodeAsMatch(DicTraverseSession *traverseSession,
             DicNode *childDicNode) const;
 
-    // Dic nodes cache size for lookahead (autocompletion)
-    static const int LOOKAHEAD_DIC_NODES_CACHE_SIZE;
-    // Max characters to lookahead
-    static const int MAX_LOOKAHEAD;
     // Inputs longer than this will autocorrect if the suggestion is multi-word
     static const int MIN_LEN_FOR_MULTI_WORD_AUTOCORRECT;
     static const int MIN_CONTINUOUS_SUGGESTION_INPUT_SIZE;
-    // Base value for converting costs into scores (low so will not autocorrect without classifier)
-    static const float BASE_OUTPUT_SCORE;
 
     // Threshold for autocorrection classifier
     static const float AUTOCORRECT_CLASSIFICATION_THRESHOLD;
-    // Threshold for computing the language model feature for autocorrect classification
-    static const float AUTOCORRECT_LANGUAGE_FEATURE_THRESHOLD;
-
-    // Typing error correction settings
-    static const bool CORRECT_SPACE_OMISSION;
-    static const bool CORRECT_TRANSPOSITION;
-    static const bool CORRECT_INSERTION;
 
     const Traversal *const TRAVERSAL;
     const Scoring *const SCORING;
     const Weighting *const WEIGHTING;
-
-    static const bool CORRECT_OMISSION_G;
 };
 } // namespace latinime
 #endif // LATINIME_SUGGEST_IMPL_H
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_traversal.cpp b/native/jni/src/suggest/policyimpl/typing/typing_traversal.cpp
index 66f8ba9..e7e40e3 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_traversal.cpp
+++ b/native/jni/src/suggest/policyimpl/typing/typing_traversal.cpp
@@ -18,7 +18,7 @@
 
 namespace latinime {
 const bool TypingTraversal::CORRECT_OMISSION = true;
-const bool TypingTraversal::CORRECT_SPACE_SUBSTITUTION = true;
-const bool TypingTraversal::CORRECT_SPACE_OMISSION = true;
+const bool TypingTraversal::CORRECT_NEW_WORD_SPACE_SUBSTITUTION = true;
+const bool TypingTraversal::CORRECT_NEW_WORD_SPACE_OMISSION = true;
 const TypingTraversal TypingTraversal::sInstance;
 }  // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
index f22029a..9f83474 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
@@ -66,7 +66,7 @@
 
     AK_FORCE_INLINE bool isSpaceSubstitutionTerminal(
             const DicTraverseSession *const traverseSession, const DicNode *const dicNode) const {
-        if (!CORRECT_SPACE_SUBSTITUTION) {
+        if (!CORRECT_NEW_WORD_SPACE_SUBSTITUTION) {
             return false;
         }
         if (!canDoLookAheadCorrection(traverseSession, dicNode)) {
@@ -80,7 +80,7 @@
 
     AK_FORCE_INLINE bool isSpaceOmissionTerminal(
             const DicTraverseSession *const traverseSession, const DicNode *const dicNode) const {
-        if (!CORRECT_SPACE_OMISSION) {
+        if (!CORRECT_NEW_WORD_SPACE_OMISSION) {
             return false;
         }
         const int inputSize = traverseSession->getInputSize();
@@ -173,8 +173,8 @@
  private:
     DISALLOW_COPY_AND_ASSIGN(TypingTraversal);
     static const bool CORRECT_OMISSION;
-    static const bool CORRECT_SPACE_SUBSTITUTION;
-    static const bool CORRECT_SPACE_OMISSION;
+    static const bool CORRECT_NEW_WORD_SPACE_SUBSTITUTION;
+    static const bool CORRECT_NEW_WORD_SPACE_OMISSION;
     static const TypingTraversal sInstance;
 
     TypingTraversal() {}
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
index 52d54eb..74e4e34 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
@@ -18,6 +18,7 @@
 #define LATINIME_TYPING_WEIGHTING_H
 
 #include "defines.h"
+#include "suggest_utils.h"
 #include "suggest/core/dicnode/dic_node_utils.h"
 #include "suggest/core/policy/weighting.h"
 #include "suggest/core/session/dic_traverse_session.h"
@@ -70,10 +71,12 @@
         const int pointIndex = dicNode->getInputIndex(0);
         // Note: min() required since length can be MAX_POINT_TO_KEY_LENGTH for characters not on
         // the keyboard (like accented letters)
-        const float length = min(ScoringParams::MAX_SPATIAL_DISTANCE,
-                traverseSession->getProximityInfoState(0)->getPointToKeyLength(
-                        pointIndex, dicNode->getNodeCodePoint()));
-        const float weightedDistance = length * ScoringParams::DISTANCE_WEIGHT_LENGTH;
+        const float normalizedSquaredLength = traverseSession->getProximityInfoState(0)
+                ->getPointToKeyLength(pointIndex, dicNode->getNodeCodePoint());
+        const float normalizedDistance = SuggestUtils::getSweetSpotFactor(
+                traverseSession->isTouchPositionCorrectionEnabled(), normalizedSquaredLength);
+        const float weightedDistance = ScoringParams::DISTANCE_WEIGHT_LENGTH * normalizedDistance;
+
         const bool isFirstChar = pointIndex == 0;
         const bool isProximity = isProximityDicNode(traverseSession, dicNode);
         const float cost = isProximity ? (isFirstChar ? ScoringParams::FIRST_PROXIMITY_COST
@@ -125,10 +128,12 @@
         return cost + weightedDistance;
     }
 
-    float getNewWordCost(const DicNode *const dicNode) const {
+    float getNewWordCost(const DicTraverseSession *const traverseSession,
+            const DicNode *const dicNode) const {
         const bool isCapitalized = dicNode->isCapitalized();
-        return isCapitalized ?
+        const float cost = isCapitalized ?
                 ScoringParams::COST_NEW_WORD_CAPITALIZED : ScoringParams::COST_NEW_WORD;
+        return cost * traverseSession->getMultiWordCostMultiplier();
     }
 
     float getNewWordBigramCost(
@@ -180,8 +185,13 @@
         return ScoringParams::SUBSTITUTION_COST;
     }
 
-    AK_FORCE_INLINE float getSpaceSubstitutionCost() const {
-        return ScoringParams::SPACE_SUBSTITUTION_COST;
+    AK_FORCE_INLINE float getSpaceSubstitutionCost(
+            const DicTraverseSession *const traverseSession,
+            const DicNode *const dicNode) const {
+        const bool isCapitalized = dicNode->isCapitalized();
+        const float cost = ScoringParams::SPACE_SUBSTITUTION_COST + (isCapitalized ?
+                ScoringParams::COST_NEW_WORD_CAPITALIZED : ScoringParams::COST_NEW_WORD);
+        return cost * traverseSession->getMultiWordCostMultiplier();
     }
 
  private:
diff --git a/native/jni/src/suggest_utils.h b/native/jni/src/suggest_utils.h
index aab9f7b..e053dd6 100644
--- a/native/jni/src/suggest_utils.h
+++ b/native/jni/src/suggest_utils.h
@@ -23,10 +23,8 @@
 namespace latinime {
 class SuggestUtils {
  public:
-    static float getDistanceScalingFactor(const float normalizedSquaredDistance) {
-        if (normalizedSquaredDistance < 0.0f) {
-            return -1.0f;
-        }
+    // TODO: (OLD) Remove
+    static float getLengthScalingFactor(const float normalizedSquaredDistance) {
         // Promote or demote the score according to the distance from the sweet spot
         static const float A = ZERO_DISTANCE_PROMOTION_RATE / 100.0f;
         static const float B = 1.0f;
@@ -50,6 +48,39 @@
         return factor;
     }
 
+    static float getSweetSpotFactor(const bool isTouchPositionCorrectionEnabled,
+            const float normalizedSquaredDistance) {
+        // Promote or demote the score according to the distance from the sweet spot
+        static const float A = 0.0f;
+        static const float B = 0.24f;
+        static const float C = 1.20f;
+        static const float R0 = 0.0f;
+        static const float R1 = 0.25f; // Sweet spot
+        static const float R2 = 1.0f;
+        const float x = normalizedSquaredDistance;
+        if (!isTouchPositionCorrectionEnabled) {
+            return min(C, x);
+        }
+
+        // factor is a piecewise linear function like:
+        // C        -------------.
+        //         /             .
+        // B      /              .
+        //      -/               .
+        // A _-^                 .
+        //                       .
+        //   R0 R1 R2            .
+
+        if (x < R0) {
+            return A;
+        } else if (x < R1) {
+            return (A * (R1 - x) + B * (x - R0)) / (R1 - R0);
+        } else if (x < R2) {
+            return (B * (R2 - x) + C * (x - R1)) / (R2 - R1);
+        } else {
+            return C;
+        }
+    }
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(SuggestUtils);
 };
diff --git a/native/jni/src/unigram_dictionary.cpp b/native/jni/src/unigram_dictionary.cpp
index 33795ca..a672294 100644
--- a/native/jni/src/unigram_dictionary.cpp
+++ b/native/jni/src/unigram_dictionary.cpp
@@ -32,9 +32,9 @@
 namespace latinime {
 
 // TODO: check the header
-UnigramDictionary::UnigramDictionary(const uint8_t *const streamStart, const unsigned int flags)
+UnigramDictionary::UnigramDictionary(const uint8_t *const streamStart, const unsigned int dictFlags)
         : DICT_ROOT(streamStart), ROOT_POS(0),
-          MAX_DIGRAPH_SEARCH_DEPTH(DEFAULT_MAX_DIGRAPH_SEARCH_DEPTH), FLAGS(flags) {
+          MAX_DIGRAPH_SEARCH_DEPTH(DEFAULT_MAX_DIGRAPH_SEARCH_DEPTH), DICT_FLAGS(dictFlags) {
     if (DEBUG_DICT) {
         AKLOGI("UnigramDictionary - constructor");
     }
@@ -163,7 +163,7 @@
     masterCorrection.resetCorrection();
     const DigraphUtils::digraph_t *digraphs = 0;
     const int digraphsSize =
-            DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(FLAGS, &digraphs);
+            DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(DICT_FLAGS, &digraphs);
     if (digraphsSize > 0)
     { // Incrementally tune the word and try all possibilities
         int codesBuffer[sizeof(*inputCodePoints) * inputSize];
diff --git a/native/jni/src/unigram_dictionary.h b/native/jni/src/unigram_dictionary.h
index 1a01758..a64a539 100644
--- a/native/jni/src/unigram_dictionary.h
+++ b/native/jni/src/unigram_dictionary.h
@@ -38,7 +38,7 @@
     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, const unsigned int flags);
+    UnigramDictionary(const uint8_t *const streamStart, const unsigned int dictFlags);
     int getProbability(const int *const inWord, const int length) const;
     int getBigramPosition(int pos, int *word, int offset, int length) const;
     int getSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
@@ -46,6 +46,7 @@
             const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
             const bool useFullEditDistance, int *outWords, int *frequencies,
             int *outputTypes) const;
+    int getDictFlags() const { return DICT_FLAGS; }
     virtual ~UnigramDictionary();
 
  private:
@@ -109,7 +110,7 @@
     const uint8_t *const DICT_ROOT;
     const int ROOT_POS;
     const int MAX_DIGRAPH_SEARCH_DEPTH;
-    const int FLAGS;
+    const int DICT_FLAGS;
 };
 } // namespace latinime
 #endif // LATINIME_UNIGRAM_DICTIONARY_H
diff --git a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
index 8fef1de..6bb5ada 100644
--- a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderFixedOrderTests.java
@@ -43,10 +43,10 @@
         super.setUp();
     }
 
-    private static MoreKeysKeyboardParams createParams(int numKeys, int columnNum,
-            int coordXInParnet) {
+    private static MoreKeysKeyboardParams createParams(final int numKeys, final int columnNum,
+            final int coordXInParent) {
         final MoreKeysKeyboardParams params = new MoreKeysKeyboardParams();
-        params.setParameters(numKeys, columnNum, WIDTH, HEIGHT, coordXInParnet, KEYBOARD_WIDTH,
+        params.setParameters(numKeys, columnNum, WIDTH, HEIGHT, coordXInParent, KEYBOARD_WIDTH,
                 /* isFixedOrderColumn */true, /* dividerWidth */0);
         return params;
     }
@@ -55,7 +55,7 @@
         MoreKeysKeyboardParams params = null;
         try {
             final int fixColumns = KEYBOARD_WIDTH / WIDTH;
-            params = createParams(10, fixColumns + 1, HEIGHT);
+            params = createParams(fixColumns + 1, fixColumns + 1, HEIGHT);
             fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException e) {
             // Too small keyboard to hold more keys keyboard.
diff --git a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
index ee4c72a..99da481 100644
--- a/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/MoreKeysKeyboardBuilderTests.java
@@ -43,10 +43,10 @@
         super.setUp();
     }
 
-    private static MoreKeysKeyboardParams createParams(int numKeys, int maxColumns,
-            int coordXInParnet) {
+    private static MoreKeysKeyboardParams createParams(final int numKeys, final int maxColumns,
+            final int coordXInParent) {
         final MoreKeysKeyboardParams params = new MoreKeysKeyboardParams();
-        params.setParameters(numKeys, maxColumns, WIDTH, HEIGHT, coordXInParnet, KEYBOARD_WIDTH,
+        params.setParameters(numKeys, maxColumns, WIDTH, HEIGHT, coordXInParent, KEYBOARD_WIDTH,
                 /* isFixedOrderColumn */false, /* dividerWidth */0);
         return params;
     }
@@ -55,7 +55,7 @@
         MoreKeysKeyboardParams params = null;
         try {
             final int maxColumns = KEYBOARD_WIDTH / WIDTH;
-            params = createParams(10, maxColumns + 1, HEIGHT);
+            params = createParams(maxColumns + 1, maxColumns + 1, HEIGHT);
             fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException e) {
             // Too small keyboard to hold more keys keyboard.
diff --git a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
index 1398db9..850af94 100644
--- a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
@@ -113,7 +113,8 @@
             final String subtypeName = SubtypeLocale.getSubtypeDisplayName(subtype);
             final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
             final String spacebarText = MainKeyboardView.getShortDisplayName(subtype);
-            final String languageCode = StringUtils.toTitleCase(locale.getLanguage(), locale);
+            final String languageCode = StringUtils.capitalizeFirstCodePoint(
+                    locale.getLanguage(), locale);
             if (SubtypeLocale.isNoLanguage(subtype)) {
                 assertEquals(subtypeName, "", spacebarText);
             } else {
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
index eb48408..74506d2 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
@@ -19,6 +19,7 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.RecapitalizeStatus;
 
 public class MockKeyboardSwitcher implements KeyboardState.SwitchActions {
     public interface MockConstants {
@@ -120,7 +121,7 @@
 
     @Override
     public void requestUpdatingShiftState() {
-        mState.onUpdateShiftState(mAutoCapsState);
+        mState.onUpdateShiftState(mAutoCapsState, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE);
     }
 
     @Override
@@ -162,7 +163,7 @@
     }
 
     public void updateShiftState() {
-        mState.onUpdateShiftState(mAutoCapsState);
+        mState.onUpdateShiftState(mAutoCapsState, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE);
     }
 
     public void loadKeyboard() {
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 4583eab..9e107a4 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -162,45 +162,22 @@
     // on the same thread that the tests are running on to mimic the actual environment as
     // closely as possible.
     // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method
-    // is called, so we need to do that at the right time so that #loop() returns at some
-    // point and we don't end up in an infinite loop.
-    // After we quit, the looper is still technically ready to process more messages but
-    // the handler will refuse to enqueue any because #quit() has been called and it
-    // explicitly tests for it on message enqueuing, so we'll have to reset it so that
-    // it lets us continue normal operation.
+    // is called, which has a lot of bad side effects. We can however just throw an exception
+    // in the runnable which will unwind the stack and allow us to exit.
+    private final class InterruptRunMessagesException extends RuntimeException {
+        // Empty class
+    }
     protected void runMessages() {
-        // Here begins deep magic.
-        final Looper looper = mLatinIME.mHandler.getLooper();
         mLatinIME.mHandler.post(new Runnable() {
                 @Override
                 public void run() {
-                    looper.quit();
+                    throw new InterruptRunMessagesException();
                 }
             });
-        // The only way to get out of Looper#loop() is to call #quit() on it (or on its queue).
-        // Once #quit() is called remaining messages are not processed, which is why we post
-        // a message that calls it instead of calling it directly.
-        Looper.loop();
-
-        // Once #quit() has been called, the looper is not functional any more (it used to be,
-        // but now it SIGSEGV's if it's used again).
-        // It won't accept creating a new looper for this thread and switching to it...
-        // ...unless we can trick it into throwing out the old looper and believing it hasn't
-        // been initialized before.
-        MessageQueue queue = Looper.myQueue();
         try {
-            // However there is no way of doing it externally, and the static ThreadLocal
-            // field into which it's stored is private.
-            // So... get out the big guns.
-            java.lang.reflect.Field f = Looper.class.getDeclaredField("sThreadLocal");
-            f.setAccessible(true); // private lolwut
-            final ThreadLocal<Looper> a = (ThreadLocal<Looper>) f.get(looper);
-            a.set(null);
-            looper.prepare();
-        } catch (NoSuchFieldException e) {
-            throw new RuntimeException(e);
-        } catch (IllegalAccessException e) {
-            throw new RuntimeException(e);
+            Looper.loop();
+        } catch (InterruptRunMessagesException e) {
+            // Resume normal operation
         }
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/RecapitalizeStatusTests.java b/tests/src/com/android/inputmethod/latin/RecapitalizeStatusTests.java
new file mode 100644
index 0000000..9d7203e
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/RecapitalizeStatusTests.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Locale;
+
+@SmallTest
+public class RecapitalizeStatusTests extends AndroidTestCase {
+    public void testTrim() {
+        final RecapitalizeStatus status = new RecapitalizeStatus();
+        status.initialize(30, 40, "abcdefghij", Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+
+        status.initialize(30, 44, "    abcdefghij", Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(34, status.getNewCursorStart());
+        assertEquals(44, status.getNewCursorEnd());
+
+        status.initialize(30, 40, "abcdefgh  ", Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefgh", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(38, status.getNewCursorEnd());
+
+        status.initialize(30, 45, "   abcdefghij  ", Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(33, status.getNewCursorStart());
+        assertEquals(43, status.getNewCursorEnd());
+    }
+
+    public void testRotate() {
+        final RecapitalizeStatus status = new RecapitalizeStatus();
+        status.initialize(29, 40, "abcd efghij", Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+
+        status.initialize(29, 40, "Abcd Efghij", Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+
+        status.initialize(29, 40, "ABCD EFGHIJ", Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+
+        status.initialize(29, 39, "AbCDefghij", Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(39, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Abcdefghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCDEFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("AbCDefghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+
+        status.initialize(29, 40, "Abcd efghij", Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("Abcd efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+
+        status.initialize(30, 34, "grüß", Locale.GERMAN, " "); status.rotate();
+        assertEquals("Grüß", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(34, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("GRÜSS", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("grüß", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(34, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Grüß", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(34, status.getNewCursorEnd());
+
+        status.initialize(30, 33, "œuf", Locale.FRENCH, " "); status.rotate();
+        assertEquals("Œuf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("ŒUF", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("œuf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Œuf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+
+        status.initialize(30, 33, "œUf", Locale.FRENCH, " "); status.rotate();
+        assertEquals("œuf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Œuf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("ŒUF", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("œUf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("œuf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+
+        status.initialize(30, 35, "école", Locale.FRENCH, " "); status.rotate();
+        assertEquals("École", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("ÉCOLE", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("école", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("École", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
index 9e545a5..aacd60f 100644
--- a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
@@ -19,6 +19,7 @@
 import android.inputmethodservice.InputMethodService;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
@@ -84,6 +85,11 @@
         public boolean endBatchEdit() {
             return true;
         }
+
+        @Override
+        public boolean finishComposingText() {
+            return true;
+        }
     }
 
     private class MockInputMethodService extends InputMethodService {
@@ -141,11 +147,11 @@
         ic.beginBatchEdit();
         // basic case
         r = ic.getWordRangeAtCursor(" ", 0);
-        assertEquals("word", r.mWord);
+        assertTrue(TextUtils.equals("word", r.mWord));
 
         // more than one word
         r = ic.getWordRangeAtCursor(" ", 1);
-        assertEquals("word word", r.mWord);
+        assertTrue(TextUtils.equals("word word", r.mWord));
         ic.endBatchEdit();
 
         // tab character instead of space
@@ -153,28 +159,28 @@
         ic.beginBatchEdit();
         r = ic.getWordRangeAtCursor("\t", 1);
         ic.endBatchEdit();
-        assertEquals("word\tword", r.mWord);
+        assertTrue(TextUtils.equals("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);
+        assertTrue(TextUtils.equals("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);
+        assertTrue(TextUtils.equals("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);
+        assertTrue(TextUtils.equals("one word\tword", r.mWord));
 
         // splitting on supplementary character
         final String supplementaryChar = "\uD840\uDC8A";
@@ -183,6 +189,6 @@
         ic.beginBatchEdit();
         r = ic.getWordRangeAtCursor(supplementaryChar, 0);
         ic.endBatchEdit();
-        assertEquals("word", r.mWord);
+        assertTrue(TextUtils.equals("word", r.mWord));
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
index 966919e..1e3cc8a 100644
--- a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
@@ -93,25 +93,43 @@
                 StringUtils.removeFromCsvIfExists("key", "key1,key,key3,key,key5"));
     }
 
-    public void testToTitleCase() {
+
+    public void testCapitalizeFirstCodePoint() {
         assertEquals("SSaa",
-                StringUtils.toTitleCase("ßaa", Locale.GERMAN));
+                StringUtils.capitalizeFirstCodePoint("ßaa", Locale.GERMAN));
         assertEquals("Aßa",
-                StringUtils.toTitleCase("aßa", Locale.GERMAN));
+                StringUtils.capitalizeFirstCodePoint("aßa", Locale.GERMAN));
         assertEquals("Iab",
-                StringUtils.toTitleCase("iab", Locale.ENGLISH));
-        assertEquals("Camelcase",
-                StringUtils.toTitleCase("cAmElCaSe", Locale.ENGLISH));
+                StringUtils.capitalizeFirstCodePoint("iab", Locale.ENGLISH));
+        assertEquals("CAmElCaSe",
+                StringUtils.capitalizeFirstCodePoint("cAmElCaSe", Locale.ENGLISH));
         assertEquals("İab",
-                StringUtils.toTitleCase("iab", new Locale("tr")));
-        assertEquals("Aib",
-                StringUtils.toTitleCase("AİB", new Locale("tr")));
-        // For one character, toTitleCase returns the string as is. Not sure what the motivation
-        // is, but that's how it works now.
-        assertEquals("a",
-                StringUtils.toTitleCase("a", Locale.ENGLISH));
+                StringUtils.capitalizeFirstCodePoint("iab", new Locale("tr")));
+        assertEquals("AİB",
+                StringUtils.capitalizeFirstCodePoint("AİB", new Locale("tr")));
         assertEquals("A",
-                StringUtils.toTitleCase("A", Locale.ENGLISH));
+                StringUtils.capitalizeFirstCodePoint("a", Locale.ENGLISH));
+        assertEquals("A",
+                StringUtils.capitalizeFirstCodePoint("A", Locale.ENGLISH));
+    }
+
+    public void testCapitalizeFirstAndDowncaseRest() {
+        assertEquals("SSaa",
+                StringUtils.capitalizeFirstAndDowncaseRest("ßaa", Locale.GERMAN));
+        assertEquals("Aßa",
+                StringUtils.capitalizeFirstAndDowncaseRest("aßa", Locale.GERMAN));
+        assertEquals("Iab",
+                StringUtils.capitalizeFirstAndDowncaseRest("iab", Locale.ENGLISH));
+        assertEquals("Camelcase",
+                StringUtils.capitalizeFirstAndDowncaseRest("cAmElCaSe", Locale.ENGLISH));
+        assertEquals("İab",
+                StringUtils.capitalizeFirstAndDowncaseRest("iab", new Locale("tr")));
+        assertEquals("Aib",
+                StringUtils.capitalizeFirstAndDowncaseRest("AİB", new Locale("tr")));
+        assertEquals("A",
+                StringUtils.capitalizeFirstAndDowncaseRest("a", Locale.ENGLISH));
+        assertEquals("A",
+                StringUtils.capitalizeFirstAndDowncaseRest("A", Locale.ENGLISH));
     }
 
     public void testGetCapitalizationType() {
@@ -136,4 +154,87 @@
         assertEquals(StringUtils.CAPITALIZE_NONE,
                 StringUtils.getCapitalizationType(""));
     }
+
+    public void testIsIdenticalAfterUpcaseIsIdenticalAfterDowncase() {
+        assertFalse(StringUtils.isIdenticalAfterUpcase("capitalize"));
+        assertTrue(StringUtils.isIdenticalAfterDowncase("capitalize"));
+        assertFalse(StringUtils.isIdenticalAfterUpcase("cApITalize"));
+        assertFalse(StringUtils.isIdenticalAfterDowncase("cApITalize"));
+        assertFalse(StringUtils.isIdenticalAfterUpcase("capitalizE"));
+        assertFalse(StringUtils.isIdenticalAfterDowncase("capitalizE"));
+        assertFalse(StringUtils.isIdenticalAfterUpcase("__c a piu$@tali56ze"));
+        assertTrue(StringUtils.isIdenticalAfterDowncase("__c a piu$@tali56ze"));
+        assertFalse(StringUtils.isIdenticalAfterUpcase("A__c a piu$@tali56ze"));
+        assertFalse(StringUtils.isIdenticalAfterDowncase("A__c a piu$@tali56ze"));
+        assertFalse(StringUtils.isIdenticalAfterUpcase("Capitalize"));
+        assertFalse(StringUtils.isIdenticalAfterDowncase("Capitalize"));
+        assertFalse(StringUtils.isIdenticalAfterUpcase("     Capitalize"));
+        assertFalse(StringUtils.isIdenticalAfterDowncase("     Capitalize"));
+        assertTrue(StringUtils.isIdenticalAfterUpcase("CAPITALIZE"));
+        assertFalse(StringUtils.isIdenticalAfterDowncase("CAPITALIZE"));
+        assertTrue(StringUtils.isIdenticalAfterUpcase("  PI26LIE"));
+        assertFalse(StringUtils.isIdenticalAfterDowncase("  PI26LIE"));
+        assertTrue(StringUtils.isIdenticalAfterUpcase(""));
+        assertTrue(StringUtils.isIdenticalAfterDowncase(""));
+    }
+
+    private void checkCapitalize(final String src, final String dst, final String separators,
+            final Locale locale) {
+        assertEquals(dst, StringUtils.capitalizeEachWord(src, separators, locale));
+        assert(src.equals(dst)
+                == StringUtils.isIdenticalAfterCapitalizeEachWord(src, separators));
+    }
+
+    public void testCapitalizeEachWord() {
+        checkCapitalize("", "", " ", Locale.ENGLISH);
+        checkCapitalize("test", "Test", " ", Locale.ENGLISH);
+        checkCapitalize("    test", "    Test", " ", Locale.ENGLISH);
+        checkCapitalize("Test", "Test", " ", Locale.ENGLISH);
+        checkCapitalize("    Test", "    Test", " ", Locale.ENGLISH);
+        checkCapitalize(".Test", ".test", " ", Locale.ENGLISH);
+        checkCapitalize(".Test", ".Test", " .", Locale.ENGLISH);
+        checkCapitalize(".Test", ".Test", ". ", Locale.ENGLISH);
+        checkCapitalize("test and retest", "Test And Retest", " .", Locale.ENGLISH);
+        checkCapitalize("Test and retest", "Test And Retest", " .", Locale.ENGLISH);
+        checkCapitalize("Test And Retest", "Test And Retest", " .", Locale.ENGLISH);
+        checkCapitalize("Test And.Retest  ", "Test And.Retest  ", " .", Locale.ENGLISH);
+        checkCapitalize("Test And.retest  ", "Test And.Retest  ", " .", Locale.ENGLISH);
+        checkCapitalize("Test And.retest  ", "Test And.retest  ", " ", Locale.ENGLISH);
+        checkCapitalize("Test And.Retest  ", "Test And.retest  ", " ", Locale.ENGLISH);
+        checkCapitalize("test and ietest", "Test And İetest", " .", new Locale("tr"));
+        checkCapitalize("test and ietest", "Test And Ietest", " .", Locale.ENGLISH);
+        checkCapitalize("Test&Retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH);
+        checkCapitalize("Test&retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH);
+        checkCapitalize("test&Retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH);
+        checkCapitalize("rest\nrecreation! And in the end...",
+                "Rest\nRecreation! And In The End...", " \n.!?*,();&", Locale.ENGLISH);
+        checkCapitalize("lorem ipsum dolor sit amet", "Lorem Ipsum Dolor Sit Amet",
+                " \n.,!?*()&;", Locale.ENGLISH);
+        checkCapitalize("Lorem!Ipsum (Dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet",
+                " \n,.;!?*()&", Locale.ENGLISH);
+        checkCapitalize("Lorem!Ipsum (dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet",
+                " \n,.;!?*()&", Locale.ENGLISH);
+    }
+
+    public void testLooksLikeURL() {
+        assertTrue(StringUtils.lastPartLooksLikeURL("http://www.google."));
+        assertFalse(StringUtils.lastPartLooksLikeURL("word wo"));
+        assertTrue(StringUtils.lastPartLooksLikeURL("/etc/foo"));
+        assertFalse(StringUtils.lastPartLooksLikeURL("left/right"));
+        assertTrue(StringUtils.lastPartLooksLikeURL("www.goo"));
+        assertTrue(StringUtils.lastPartLooksLikeURL("www."));
+        assertFalse(StringUtils.lastPartLooksLikeURL("U.S.A"));
+        assertFalse(StringUtils.lastPartLooksLikeURL("U.S.A."));
+        assertTrue(StringUtils.lastPartLooksLikeURL("rtsp://foo."));
+        assertTrue(StringUtils.lastPartLooksLikeURL("://"));
+        assertFalse(StringUtils.lastPartLooksLikeURL("abc/"));
+        assertTrue(StringUtils.lastPartLooksLikeURL("abc.def/ghi"));
+        assertFalse(StringUtils.lastPartLooksLikeURL("abc.def"));
+        // TODO: ideally this would not look like a URL, but to keep down the complexity of the
+        // code for now True is acceptable.
+        assertTrue(StringUtils.lastPartLooksLikeURL("abc./def"));
+        // TODO: ideally this would not look like a URL, but to keep down the complexity of the
+        // code for now True is acceptable.
+        assertTrue(StringUtils.lastPartLooksLikeURL(".abc/def"));
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
new file mode 100644
index 0000000..9162522
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+@SmallTest
+public class SuggestedWordsTests extends AndroidTestCase {
+    public void testGetSuggestedWordsExcludingTypedWord() {
+        final String TYPED_WORD = "typed";
+        final int TYPED_WORD_FREQ = 5;
+        final int NUMBER_OF_ADDED_SUGGESTIONS = 5;
+        final ArrayList<SuggestedWordInfo> list = CollectionUtils.newArrayList();
+        list.add(new SuggestedWordInfo(TYPED_WORD, TYPED_WORD_FREQ,
+                SuggestedWordInfo.KIND_TYPED, ""));
+        for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) {
+            list.add(new SuggestedWordInfo("" + i, 1, SuggestedWordInfo.KIND_CORRECTION, ""));
+        }
+
+        final SuggestedWords words = new SuggestedWords(
+                list,
+                false /* typedWordValid */,
+                false /* willAutoCorrect */,
+                false /* isPunctuationSuggestions */,
+                false /* isObsoleteSuggestions */,
+                false /* isPrediction*/);
+        assertEquals(NUMBER_OF_ADDED_SUGGESTIONS + 1, words.size());
+        assertEquals("typed", words.getWord(0));
+        assertEquals(SuggestedWordInfo.KIND_TYPED, words.getInfo(0).mKind);
+        assertEquals("0", words.getWord(1));
+        assertEquals(SuggestedWordInfo.KIND_CORRECTION, words.getInfo(1).mKind);
+        assertEquals("4", words.getWord(5));
+        assertEquals(SuggestedWordInfo.KIND_CORRECTION, words.getInfo(5).mKind);
+
+        final SuggestedWords wordsWithoutTyped = words.getSuggestedWordsExcludingTypedWord();
+        assertEquals(words.size() - 1, wordsWithoutTyped.size());
+        assertEquals("0", wordsWithoutTyped.getWord(0));
+        assertEquals(SuggestedWordInfo.KIND_CORRECTION, wordsWithoutTyped.getInfo(0).mKind);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
index bd87292..b704d08 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
@@ -51,7 +51,7 @@
 @LargeTest
 public class BinaryDictIOTests extends AndroidTestCase {
     private static final String TAG = BinaryDictIOTests.class.getSimpleName();
-    private static final int MAX_UNIGRAMS = 1000;
+    private static final int MAX_UNIGRAMS = 100;
     private static final int UNIGRAM_FREQ = 10;
     private static final int BIGRAM_FREQ = 50;
     private static final int TOLERANCE_OF_BIGRAM_FREQ = 5;
@@ -135,9 +135,13 @@
         while (count > 0) {
             final long r = Math.abs(random.nextInt());
             if (r < 0) continue;
-            // Don't insert 0~20, but insert any other code point.
+            // Don't insert 0~0x20, but insert any other code point.
             // Code points are in the range 0~0x10FFFF.
-            builder.appendCodePoint((int)(20 + r % (0x10FFFF - 20)));
+            final int candidateCodePoint = (int)(0x20 + r % (Character.MAX_CODE_POINT - 0x20));
+            // Code points between MIN_ and MAX_SURROGATE are not valid on their own.
+            if (candidateCodePoint >= Character.MIN_SURROGATE
+                    && candidateCodePoint <= Character.MAX_SURROGATE) continue;
+            builder.appendCodePoint(candidateCodePoint);
             --count;
         }
         return builder.toString();