Merge "Support ICS on the user dictionary settings"
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index 17d11c0..2273394 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -29,13 +29,13 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
 
-    <application android:label="@string/aosp_android_keyboard_ime_name"
+    <application android:label="@string/english_ime_name"
             android:icon="@mipmap/ic_ime_settings"
             android:killAfterRestore="false"
             android:supportsRtl="true">
 
         <service android:name="LatinIME"
-                android:label="@string/aosp_android_keyboard_ime_name"
+                android:label="@string/english_ime_name"
                 android:permission="android.permission.BIND_INPUT_METHOD">
             <intent-filter>
                 <action android:name="android.view.InputMethod" />
@@ -44,7 +44,7 @@
         </service>
 
         <service android:name=".spellcheck.AndroidSpellCheckerService"
-                 android:label="@string/aosp_spell_checker_service_name"
+                 android:label="@string/spell_checker_service_name"
                  android:permission="android.permission.BIND_TEXT_SERVICE">
             <intent-filter>
                 <action android:name="android.service.textservice.SpellCheckerService" />
@@ -53,7 +53,7 @@
         </service>
 
         <activity android:name=".setup.SetupActivity"
-                android:label="@string/aosp_android_keyboard_ime_name"
+                android:label="@string/english_ime_name"
                 android:icon="@drawable/ic_setup_wizard">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -69,7 +69,7 @@
             </intent-filter>
         </receiver>
 
-        <activity android:name="SettingsActivity" android:label="@string/aosp_android_keyboard_ime_settings"
+        <activity android:name="SettingsActivity" android:label="@string/english_ime_settings"
                   android:uiOptions="splitActionBarWhenNarrow">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -77,7 +77,7 @@
         </activity>
 
         <activity android:name="com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsActivity"
-                  android:label="@string/aosp_android_spell_checker_service_settings">
+                  android:label="@string/android_spell_checker_settings">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
diff --git a/java/res/layout/dictionary_line.xml b/java/res/layout/dictionary_line.xml
index 9eeea69..9ddbcf7 100644
--- a/java/res/layout/dictionary_line.xml
+++ b/java/res/layout/dictionary_line.xml
@@ -84,5 +84,21 @@
         android:singleLine="true"
         android:textAppearance="?android:attr/textAppearanceMedium"
         android:text="@string/install_dict" />
+    <Button
+        android:id="@+android:id/dict_cancel_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="right|center_vertical"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/cancel_download_dict" />
+    <Button
+        android:id="@+android:id/dict_delete_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="right|center_vertical"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/delete_dict" />
   </com.android.inputmethod.dictionarypack.ButtonSwitcher>
 </LinearLayout>
diff --git a/java/res/raw/empty.dict b/java/res/raw/empty.dict
index da1bf96..80ce066 100644
--- a/java/res/raw/empty.dict
+++ b/java/res/raw/empty.dict
@@ -1 +1 @@
-x±
\ No newline at end of file
+›Á:þ
\ No newline at end of file
diff --git a/java/res/values/strings-appname.xml b/java/res/values/strings-appname.xml
new file mode 100644
index 0000000..46d8c44
--- /dev/null
+++ b/java/res/values/strings-appname.xml
@@ -0,0 +1,37 @@
+<?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>
+    <!-- Application name for opensource Android keyboard. AOSP(Android Open Source Project) should not be translated.
+         This resource should be copied from msgid="8250992613616792321" -->
+    <string name="english_ime_name">Android Keyboard (AOSP)</string>
+
+    <!-- Name of Android spell checker service. AOSP(Android Open Source Project) should not be translated.
+         This resource should be copied from msgid="511950477199948048" -->
+    <string name="spell_checker_service_name">Android Spell Checker (AOSP)</string>
+
+    <!-- Title for Android Keyboard settings screen. AOSP(Android Open Source Project) should not be translated.
+         This resource should be copied from msgid="423615877174850267" -->
+    <string name="english_ime_settings">Android Keyboard Settings (AOSP)</string>
+
+    <!-- Title for the spell checking service settings screen. AOSP(Android Open Source Project) should not be translated.
+         This resource should be copied from msgid="2970535894327288421" -->
+    <string name="android_spell_checker_settings">Android Spell Checker Settings (AOSP)</string>
+</resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 1483939..9bf4ddd 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -18,18 +18,6 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Application name for opensource Android keyboard. AOSP(Android Open Source Project) should not be translated. -->
-    <string name="aosp_android_keyboard_ime_name">Android Keyboard (AOSP)</string>
-
-    <!-- Title for Android Keyboard settings screen. AOSP(Android Open Source Project) should not be translated. -->
-    <string name="aosp_android_keyboard_ime_settings">Android Keyboard Settings (AOSP)</string>
-
-    <!-- Name of Android spell checker service. AOSP(Android Open Source Project) should not be translated. -->
-    <string name="aosp_spell_checker_service_name">Android Spell Checker (AOSP)</string>
-
-    <!-- Title for the spell checking service settings screen. AOSP(Android Open Source Project) should not be translated. -->
-    <string name="aosp_android_spell_checker_service_settings">Android Spell Checker Settings (AOSP)</string>
-
     <!-- Title for Latin keyboard input options dialog [CHAR LIMIT=25] -->
     <string name="english_ime_input_options">Input options</string>
 
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index 436e080..dad7e20 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -94,8 +94,10 @@
         <item name="showMoreKeysKeyboardAtTouchedPoint">@bool/config_show_more_keys_keyboard_at_touched_point</item>
         <item name="languageOnSpacebarFinalAlpha">@integer/config_language_on_spacebar_final_alpha</item>
         <item name="languageOnSpacebarFadeoutAnimator">@anim/language_on_spacebar_fadeout</item>
+        <!-- Remove animations for now because it could drain a non-negligible amount of battery while typing.
         <item name="altCodeKeyWhileTypingFadeoutAnimator">@anim/alt_code_key_while_typing_fadeout</item>
         <item name="altCodeKeyWhileTypingFadeinAnimator">@anim/alt_code_key_while_typing_fadein</item>
+        -->
         <!-- Common attributes of MainKeyboardView for gesture typing detection and recognition -->
         <item name="gestureFloatingPreviewTextLingerTimeout">@integer/config_gesture_floating_preview_text_linger_timeout</item>
         <item name="gestureStaticTimeThresholdAfterFastTyping">@integer/config_gesture_static_time_threshold_after_fast_typing</item>
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index faf5d3c..a9d7992 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -138,7 +138,12 @@
             if (null == manager) return;
 
             // This is an upgraded word list: we should download it.
-            final Uri uri = Uri.parse(mWordList.mRemoteFilename);
+            // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+            // DownloadManager also stupidly cuts the extension to replace with its own that it
+            // gets from the content-type. We need to circumvent this.
+            final String disambiguator = "#" + System.currentTimeMillis()
+                    + com.android.inputmethod.latin.Utils.getVersionName(context) + ".dict";
+            final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
             final Request request = new Request(uri);
 
             final Resources res = context.getResources();
diff --git a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
index a062298..391a15c 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
@@ -16,9 +16,12 @@
 
 package com.android.inputmethod.dictionarypack;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewPropertyAnimator;
 import android.widget.Button;
 import android.widget.FrameLayout;
 
@@ -28,10 +31,24 @@
  * A view that handles buttons inside it according to a status.
  */
 public class ButtonSwitcher extends FrameLayout {
+    public static final int NOT_INITIALIZED = -1;
+    public static final int STATUS_NO_BUTTON = 0;
+    public static final int STATUS_INSTALL = 1;
+    public static final int STATUS_CANCEL = 2;
+    public static final int STATUS_DELETE = 3;
+    // One of the above
+    private int mStatus = NOT_INITIALIZED;
+    private int mAnimateToStatus = NOT_INITIALIZED;
+
     // Animation directions
     public static final int ANIMATION_IN = 1;
     public static final int ANIMATION_OUT = 2;
 
+    private Button mInstallButton;
+    private Button mCancelButton;
+    private Button mDeleteButton;
+    private OnClickListener mOnClickListener;
+
     public ButtonSwitcher(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -40,30 +57,96 @@
         super(context, attrs, defStyle);
     }
 
-    public void setText(final CharSequence text) {
-        ((Button)findViewById(R.id.dict_install_button)).setText(text);
+    @Override
+    protected void onLayout(final boolean changed, final int left, final int top, final int right,
+            final int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mInstallButton = (Button)findViewById(R.id.dict_install_button);
+        mCancelButton = (Button)findViewById(R.id.dict_cancel_button);
+        mDeleteButton = (Button)findViewById(R.id.dict_delete_button);
+        mInstallButton.setOnClickListener(mOnClickListener);
+        mCancelButton.setOnClickListener(mOnClickListener);
+        mDeleteButton.setOnClickListener(mOnClickListener);
+        setButtonPositionWithoutAnimation(mStatus);
+        if (mAnimateToStatus != NOT_INITIALIZED) {
+            // We have been asked to animate before we were ready, so we took a note of it.
+            // We are now ready: launch the animation.
+            animateButtonPosition(mStatus, mAnimateToStatus);
+            mStatus = mAnimateToStatus;
+            mAnimateToStatus = NOT_INITIALIZED;
+        }
     }
 
-    public void setInternalButtonVisiblility(final int visibility) {
-        findViewById(R.id.dict_install_button).setVisibility(visibility);
+    private Button getButton(final int status) {
+        switch(status) {
+        case STATUS_INSTALL:
+            return mInstallButton;
+        case STATUS_CANCEL:
+            return mCancelButton;
+        case STATUS_DELETE:
+            return mDeleteButton;
+        default:
+            return null;
+        }
+    }
+
+    public void setStatusAndUpdateVisuals(final int status) {
+        if (mStatus == NOT_INITIALIZED) {
+            setButtonPositionWithoutAnimation(status);
+            mStatus = status;
+        } else {
+            if (null == mInstallButton) {
+                // We may come here before we have been layout. In this case we don't know our
+                // size yet so we can't start animations so we need to remember what animation to
+                // start once layout has gone through.
+                mAnimateToStatus = status;
+            } else {
+                animateButtonPosition(mStatus, status);
+                mStatus = status;
+            }
+        }
+    }
+
+    private void setButtonPositionWithoutAnimation(final int status) {
+        // This may be called by setStatus() before the layout has come yet.
+        if (null == mInstallButton) return;
+        final int width = getWidth();
+        // Set to out of the screen if that's not the currently displayed status
+        mInstallButton.setTranslationX(STATUS_INSTALL == status ? 0 : width);
+        mCancelButton.setTranslationX(STATUS_CANCEL == status ? 0 : width);
+        mDeleteButton.setTranslationX(STATUS_DELETE == status ? 0 : width);
+    }
+
+    private void animateButtonPosition(final int oldStatus, final int newStatus) {
+        final View oldButton = getButton(oldStatus);
+        final View newButton = getButton(newStatus);
+        if (null != oldButton && null != newButton) {
+            // Transition between two buttons : animate out, then in
+            animateButton(oldButton, ANIMATION_OUT).setListener(
+                    new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(final Animator animation) {
+                            animateButton(newButton, ANIMATION_IN);
+                        }
+                    });
+        } else if (null != oldButton) {
+            animateButton(oldButton, ANIMATION_OUT);
+        } else if (null != newButton) {
+            animateButton(newButton, ANIMATION_IN);
+        }
     }
 
     public void setInternalOnClickListener(final OnClickListener listener) {
-        findViewById(R.id.dict_install_button).setOnClickListener(listener);
+        mOnClickListener = listener;
     }
 
-    public void animateButton(final int direction) {
-        final View button = findViewById(R.id.dict_install_button);
+    private ViewPropertyAnimator animateButton(final View button, final int direction) {
         final float outerX = getWidth();
         final float innerX = button.getX() - button.getTranslationX();
-        if (View.INVISIBLE == button.getVisibility()) {
-            button.setTranslationX(outerX - innerX);
-            button.setVisibility(View.VISIBLE);
-        }
         if (ANIMATION_IN == direction) {
-            button.animate().translationX(0);
+            return button.animate().translationX(0);
         } else {
-            button.animate().translationX(outerX - innerX);
+            return button.animate().translationX(outerX - innerX);
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
index 8975d69..de3711c 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
@@ -44,6 +44,12 @@
         return state.mOpen;
     }
 
+    public int getStatus(final String wordlistId) {
+        final State state = mWordlistToState.get(wordlistId);
+        if (null == state) return MetadataDbHelper.STATUS_UNKNOWN;
+        return state.mStatus;
+    }
+
     public void setOpen(final String wordlistId, final int status) {
         final State newState;
         final State state = mWordlistToState.get(wordlistId);
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index a596609..3f917f1 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -212,7 +212,12 @@
     private static void updateClientsWithMetadataUri(final Context context,
             final boolean updateNow, final String metadataUri) {
         PrivateLog.log("Update for metadata URI " + Utils.s(metadataUri));
-        final Request metadataRequest = new Request(Uri.parse(metadataUri));
+        // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+        // DownloadManager also stupidly cuts the extension to replace with its own that it
+        // gets from the content-type. We need to circumvent this.
+        final String disambiguator = "#" + System.currentTimeMillis()
+                + com.android.inputmethod.latin.Utils.getVersionName(context) + ".json";
+        final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
         Utils.l("Request =", metadataRequest);
 
         final Resources res = context.getResources();
@@ -351,7 +356,13 @@
                 final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI);
                 final int error = cursor.getInt(columnError);
                 status = cursor.getInt(columnStatus);
-                uri = cursor.getString(columnUri);
+                final String uriWithAnchor = cursor.getString(columnUri);
+                int anchorIndex = uriWithAnchor.indexOf('#');
+                if (anchorIndex != -1) {
+                    uri = uriWithAnchor.substring(0, anchorIndex);
+                } else {
+                    uri = uriWithAnchor;
+                }
                 if (DownloadManager.STATUS_SUCCESSFUL != status) {
                     Log.e(TAG, "Permanent failure of download " + downloadId
                             + " with error code: " + error);
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index f1c0228..1cf9196 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -110,29 +110,31 @@
         }
     }
 
+    // The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses
+    // the values as indices.
     private static final int sStatusActionList[][] = {
         // MetadataDbHelper.STATUS_UNKNOWN
         {},
         // MetadataDbHelper.STATUS_AVAILABLE
-        { R.string.install_dict, ACTION_ENABLE_DICT },
+        { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT },
         // MetadataDbHelper.STATUS_DOWNLOADING
-        { R.string.cancel_download_dict, ACTION_DISABLE_DICT },
+        { ButtonSwitcher.STATUS_CANCEL, ACTION_DISABLE_DICT },
         // MetadataDbHelper.STATUS_INSTALLED
-        { R.string.delete_dict, ACTION_DELETE_DICT },
+        { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
         // MetadataDbHelper.STATUS_DISABLED
-        { R.string.delete_dict, ACTION_DELETE_DICT },
+        { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
         // MetadataDbHelper.STATUS_DELETING
         // We show 'install' because the file is supposed to be deleted.
         // The user may reinstall it.
-        { R.string.install_dict, ACTION_ENABLE_DICT }
+        { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT }
     };
 
-    private CharSequence getButtonLabel(final int status) {
+    private int getButtonSwitcherStatus(final int status) {
         if (status >= sStatusActionList.length) {
             Log.e(TAG, "Unknown status " + status);
-            return "";
+            return ButtonSwitcher.STATUS_NO_BUTTON;
         }
-        return mContext.getString(sStatusActionList[status][0]);
+        return sStatusActionList[status][0];
     }
 
     private static int getActionIdFromStatusAndMenuEntry(final int status) {
@@ -189,9 +191,20 @@
         ((ViewGroup)view).setLayoutTransition(null);
         final ButtonSwitcher buttonSwitcher =
                 (ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher);
-        buttonSwitcher.setText(getButtonLabel(mStatus));
-        buttonSwitcher.setInternalButtonVisiblility(mInterfaceState.isOpen(mWordlistId) ?
-                View.VISIBLE : View.INVISIBLE);
+        if (mInterfaceState.isOpen(mWordlistId)) {
+            // The button is open.
+            final int previousStatus = mInterfaceState.getStatus(mWordlistId);
+            buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(previousStatus));
+            if (previousStatus != mStatus) {
+                // We come here if the status has changed since last time. We need to animate
+                // the transition.
+                buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
+                mInterfaceState.setOpen(mWordlistId, mStatus);
+            }
+        } else {
+            // The button is closed.
+            buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
+        }
         buttonSwitcher.setInternalOnClickListener(mActionButtonClickHandler);
         view.setOnClickListener(mPreferenceClickHandler);
     }
@@ -224,9 +237,9 @@
                 final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i)
                         .findViewById(R.id.wordlist_button_switcher);
                 if (i == indexToOpen) {
-                    buttonSwitcher.animateButton(ButtonSwitcher.ANIMATION_IN);
+                    buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
                 } else {
-                    buttonSwitcher.animateButton(ButtonSwitcher.ANIMATION_OUT);
+                    buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
                 }
             }
         }
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index d74644d..8d4beec 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -333,6 +333,10 @@
 
         private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
                 final ObjectAnimator animatorToStart) {
+            if (animatorToCancel == null || animatorToStart == null) {
+                // TODO: Stop using null as a no-operation animator.
+                return;
+            }
             float startFraction = 0.0f;
             if (animatorToCancel.isStarted()) {
                 animatorToCancel.cancel();
@@ -581,6 +585,7 @@
 
     private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
         if (resId == 0) {
+            // TODO: Stop returning null.
             return null;
         }
         final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
@@ -1054,6 +1059,7 @@
 
     @Override
     public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
+        locatePreviewPlacerView();
         if (isShowingMoreKeysPanel()) {
             onDismissMoreKeysPanel();
         }
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index dbc2b90..03f7d1c 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -147,10 +147,20 @@
                 ++len;
             }
             if (len > 0) {
-                final int score = SuggestedWordInfo.KIND_WHITELIST == mOutputTypes[j]
+                final int flags = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_FLAGS;
+                if (0 != (flags & SuggestedWordInfo.KIND_FLAG_POSSIBLY_OFFENSIVE)
+                        && 0 == (flags & SuggestedWordInfo.KIND_FLAG_EXACT_MATCH)) {
+                    // If the word is possibly offensive, we don't output it unless it's also
+                    // an exact match.
+                    continue;
+                }
+                final int kind = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_KIND;
+                final int score = SuggestedWordInfo.KIND_WHITELIST == kind
                         ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j];
+                // TODO: check that all users of the `kind' parameter are ready to accept
+                // flags too and pass mOutputTypes[j] instead of kind
                 suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len),
-                        score, mOutputTypes[j], mDictType));
+                        score, kind, mDictType));
             }
         }
         return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 2943128..ddd72f1 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -72,10 +72,16 @@
     public static String getTempFileName(final String id, final Context context)
             throws IOException {
         final String safeId = DictionaryInfoUtils.replaceFileNameDangerousCharacters(id);
+        final File directory = new File(DictionaryInfoUtils.getWordListTempDirectory(context));
+        if (!directory.exists()) {
+            if (!directory.mkdirs()) {
+                Log.e(TAG, "Could not create the temporary directory");
+            }
+        }
         // If the first argument is less than three chars, createTempFile throws a
         // RuntimeException. We don't really care about what name we get, so just
         // put a three-chars prefix makes us safe.
-        return File.createTempFile("xxx" + safeId, null).getAbsolutePath();
+        return File.createTempFile("xxx" + safeId, null, directory).getAbsolutePath();
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index 9d47941..5969a63 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -122,7 +122,7 @@
         }
         boolean isDebugMode = mDebugMode.isChecked();
         final String version = getResources().getString(
-                R.string.version_text, Utils.getSdkVersion(getActivity()));
+                R.string.version_text, Utils.getVersionName(getActivity()));
         if (!isDebugMode) {
             mDebugMode.setTitle(version);
             mDebugMode.setSummary("");
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
index dcfa483..df7bad8 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
@@ -129,6 +129,13 @@
     }
 
     /**
+     * Helper method to get the top level temp directory.
+     */
+    public static String getWordListTempDirectory(final Context context) {
+        return context.getFilesDir() + File.separator + "tmp";
+    }
+
+    /**
      * Reverse escaping done by replaceFileNameDangerousCharacters.
      */
     public static String getWordListIdFromFileName(final String fname) {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 616e191..dfddb0f 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -122,6 +122,7 @@
 
     public static final class SuggestedWordInfo {
         public static final int MAX_SCORE = Integer.MAX_VALUE;
+        public static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
         public static final int KIND_TYPED = 0; // What user typed
         public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
         public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
@@ -132,6 +133,11 @@
         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 static final int KIND_MASK_FLAGS = 0xFFFFFF00; // Mask to get the flags
+        public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
+        public static final int KIND_FLAG_EXACT_MATCH = 0x40000000;
+
         public final String mWord;
         public final int mScore;
         public final int mKind; // one of the KIND_* constants above
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index fc32bd4..0f96c54 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -475,7 +475,7 @@
         return 0;
     }
 
-    public static String getSdkVersion(Context context) {
+    public static String getVersionName(Context context) {
         try {
             if (context == null) {
                 return "";
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
index 578787d..29ee63d 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
@@ -236,8 +236,7 @@
         final Intent intent = new Intent();
         intent.setClass(this, SettingsActivity.class);
         intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
-                | Intent.FLAG_ACTIVITY_CLEAR_TOP
-                | Intent.FLAG_ACTIVITY_NO_HISTORY);
+                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
         startActivity(intent);
     }
 
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index e6719c9..d3b351f 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -438,4 +438,24 @@
     // Create new word with space substitution
     CT_NEW_WORD_SPACE_SUBSTITUTION,
 } CorrectionType;
+
+// ErrorType is mainly decided by CorrectionType but it is also depending on if
+// the correction has really been performed or not.
+typedef enum {
+    // Substitution, omission and transposition
+    ET_EDIT_CORRECTION,
+    // Proximity error
+    ET_PROXIMITY_CORRECTION,
+    // Completion
+    ET_COMPLETION,
+    // New word
+    // TODO: Remove.
+    // A new word error should be an edit correction error or a proximity correction error.
+    ET_NEW_WORD,
+    // Treat error as an intentional omission when the CorrectionType is omission and the node can
+    // be intentional omission.
+    ET_INTENTIONAL_OMISSION,
+    // Not treated as an error. Tracked for checking exact match
+    ET_NOT_AN_ERROR
+} ErrorType;
 #endif // LATINIME_DEFINES_H
diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h
index 0653d3c..2ad5b6c 100644
--- a/native/jni/src/dictionary.h
+++ b/native/jni/src/dictionary.h
@@ -31,6 +31,7 @@
 class Dictionary {
  public:
     // Taken from SuggestedWords.java
+    static const int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
     static const int KIND_TYPED = 0; // What user typed
     static const int KIND_CORRECTION = 1; // Simple correction/suggestion
     static const int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
@@ -41,6 +42,10 @@
     static const int KIND_SHORTCUT = 7; // A shortcut
     static const int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
 
+    static const int KIND_MASK_FLAGS = 0xFFFFFF00; // Mask to get the flags
+    static const int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
+    static const int KIND_FLAG_EXACT_MATCH = 0x40000000;
+
     Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust);
 
     int getSuggestions(ProximityInfo *proximityInfo, void *traverseSession, int *xcoordinates,
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index e843254..92783de 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -463,6 +463,10 @@
         mDicNodeState.mDicNodeStateScoring.advanceDigraphIndex();
     }
 
+    bool isExactMatch() const {
+        return mDicNodeState.mDicNodeStateScoring.isExactMatch();
+    }
+
     uint8_t getFlags() const {
         return mDicNodeProperties.getFlags();
     }
@@ -542,13 +546,12 @@
     // Caveat: Must not be called outside Weighting
     // This restriction is guaranteed by "friend"
     AK_FORCE_INLINE void addCost(const float spatialCost, const float languageCost,
-            const bool doNormalization, const int inputSize, const bool isEditCorrection,
-            const bool isProximityCorrection) {
+            const bool doNormalization, const int inputSize, const ErrorType errorType) {
         if (DEBUG_GEO_FULL) {
             LOGI_SHOW_ADD_COST_PROP;
         }
         mDicNodeState.mDicNodeStateScoring.addCost(spatialCost, languageCost, doNormalization,
-                inputSize, getTotalInputIndex(), isEditCorrection, isProximityCorrection);
+                inputSize, getTotalInputIndex(), errorType);
     }
 
     // Caveat: Must not be called outside Weighting
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_state_input.h b/native/jni/src/suggest/core/dicnode/dic_node_state_input.h
index 7ad3e3e..bbd9435 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_state_input.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_state_input.h
@@ -46,8 +46,8 @@
         for (int i = 0; i < MAX_POINTER_COUNT_G; i++) {
              mInputIndex[i] = src->mInputIndex[i];
              mPrevCodePoint[i] = src->mPrevCodePoint[i];
-        mTerminalDiffCost[i] = resetTerminalDiffCost ?
-                static_cast<float>(MAX_VALUE_FOR_WEIGHTING) : src->mTerminalDiffCost[i];
+             mTerminalDiffCost[i] = resetTerminalDiffCost ?
+                     static_cast<float>(MAX_VALUE_FOR_WEIGHTING) : src->mTerminalDiffCost[i];
          }
     }
 
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 fd9d610..dca9d60 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
@@ -31,7 +31,7 @@
               mDigraphIndex(DigraphUtils::NOT_A_DIGRAPH_INDEX),
               mEditCorrectionCount(0), mProximityCorrectionCount(0),
               mNormalizedCompoundDistance(0.0f), mSpatialDistance(0.0f), mLanguageDistance(0.0f),
-              mRawLength(0.0f) {
+              mRawLength(0.0f), mExactMatch(true) {
     }
 
     virtual ~DicNodeStateScoring() {}
@@ -45,6 +45,7 @@
         mRawLength = 0.0f;
         mDoubleLetterLevel = NOT_A_DOUBLE_LETTER;
         mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX;
+        mExactMatch = true;
     }
 
     AK_FORCE_INLINE void init(const DicNodeStateScoring *const scoring) {
@@ -56,17 +57,32 @@
         mRawLength = scoring->mRawLength;
         mDoubleLetterLevel = scoring->mDoubleLetterLevel;
         mDigraphIndex = scoring->mDigraphIndex;
+        mExactMatch = scoring->mExactMatch;
     }
 
     void addCost(const float spatialCost, const float languageCost, const bool doNormalization,
-            const int inputSize, const int totalInputIndex, const bool isEditCorrection,
-            const bool isProximityCorrection) {
+            const int inputSize, const int totalInputIndex, const ErrorType errorType) {
         addDistance(spatialCost, languageCost, doNormalization, inputSize, totalInputIndex);
-        if (isEditCorrection) {
-            ++mEditCorrectionCount;
-        }
-        if (isProximityCorrection) {
-            ++mProximityCorrectionCount;
+        switch (errorType) {
+            case ET_EDIT_CORRECTION:
+                ++mEditCorrectionCount;
+                mExactMatch = false;
+                break;
+            case ET_PROXIMITY_CORRECTION:
+                ++mProximityCorrectionCount;
+                mExactMatch = false;
+                break;
+            case ET_COMPLETION:
+                mExactMatch = false;
+                break;
+            case ET_NEW_WORD:
+                mExactMatch = false;
+                break;
+            case ET_INTENTIONAL_OMISSION:
+                mExactMatch = false;
+                break;
+            case ET_NOT_AN_ERROR:
+                break;
         }
     }
 
@@ -143,6 +159,10 @@
         }
     }
 
+    bool isExactMatch() const {
+        return mExactMatch;
+    }
+
  private:
     // Caution!!!
     // Use a default copy constructor and an assign operator because shallow copies are ok
@@ -157,6 +177,7 @@
     float mSpatialDistance;
     float mLanguageDistance;
     float mRawLength;
+    bool mExactMatch;
 
     AK_FORCE_INLINE void addDistance(float spatialDistance, float languageDistance,
             bool doNormalization, int inputSize, int totalInputIndex) {
diff --git a/native/jni/src/suggest/core/policy/traversal.h b/native/jni/src/suggest/core/policy/traversal.h
index 02c358a..d3146da 100644
--- a/native/jni/src/suggest/core/policy/traversal.h
+++ b/native/jni/src/suggest/core/policy/traversal.h
@@ -28,7 +28,8 @@
     virtual int getMaxPointerCount() const = 0;
     virtual bool allowsErrorCorrections(const DicNode *const dicNode) const = 0;
     virtual bool isOmission(const DicTraverseSession *const traverseSession,
-            const DicNode *const dicNode, const DicNode *const childDicNode) const = 0;
+            const DicNode *const dicNode, const DicNode *const childDicNode,
+            const bool allowsErrorCorrections) const = 0;
     virtual bool isSpaceSubstitutionTerminal(const DicTraverseSession *const traverseSession,
             const DicNode *const dicNode) const = 0;
     virtual bool isSpaceOmissionTerminal(const DicTraverseSession *const traverseSession,
diff --git a/native/jni/src/suggest/core/policy/weighting.cpp b/native/jni/src/suggest/core/policy/weighting.cpp
index 6c08e76..857ddcc 100644
--- a/native/jni/src/suggest/core/policy/weighting.cpp
+++ b/native/jni/src/suggest/core/policy/weighting.cpp
@@ -80,9 +80,8 @@
             traverseSession, parentDicNode, dicNode, &inputStateG);
     const float languageCost = Weighting::getLanguageCost(weighting, correctionType,
             traverseSession, parentDicNode, dicNode, bigramCacheMap);
-    const bool edit = Weighting::isEditCorrection(correctionType);
-    const bool proximity = Weighting::isProximityCorrection(weighting, correctionType,
-            traverseSession, dicNode);
+    const ErrorType errorType = weighting->getErrorType(correctionType, traverseSession,
+            parentDicNode, dicNode);
     profile(correctionType, dicNode);
     if (inputStateG.mNeedsToUpdateInputStateG) {
         dicNode->updateInputIndexG(&inputStateG);
@@ -91,7 +90,7 @@
                 (correctionType == CT_TRANSPOSITION));
     }
     dicNode->addCost(spatialCost, languageCost, weighting->needsToNormalizeCompoundDistance(),
-            inputSize, edit, proximity);
+            inputSize, errorType);
 }
 
 /* static */ float Weighting::getSpatialCost(const Weighting *const weighting,
@@ -158,62 +157,6 @@
     }
 }
 
-/* static */ bool Weighting::isEditCorrection(const CorrectionType correctionType) {
-    switch(correctionType) {
-        case CT_OMISSION:
-            return true;
-        case CT_ADDITIONAL_PROXIMITY:
-            return true;
-        case CT_SUBSTITUTION:
-            return true;
-        case CT_NEW_WORD_SPACE_OMITTION:
-            return false;
-        case CT_MATCH:
-            return false;
-        case CT_COMPLETION:
-            return false;
-        case CT_TERMINAL:
-            return false;
-        case CT_NEW_WORD_SPACE_SUBSTITUTION:
-            return false;
-        case CT_INSERTION:
-            return true;
-        case CT_TRANSPOSITION:
-            return true;
-        default:
-            return false;
-    }
-}
-
-/* static */ bool Weighting::isProximityCorrection(const Weighting *const weighting,
-        const CorrectionType correctionType,
-        const DicTraverseSession *const traverseSession, const DicNode *const dicNode) {
-    switch(correctionType) {
-        case CT_OMISSION:
-            return false;
-        case CT_ADDITIONAL_PROXIMITY:
-            return true;
-        case CT_SUBSTITUTION:
-            return false;
-        case CT_NEW_WORD_SPACE_OMITTION:
-            return false;
-        case CT_MATCH:
-            return weighting->isProximityDicNode(traverseSession, dicNode);
-        case CT_COMPLETION:
-            return false;
-        case CT_TERMINAL:
-            return false;
-        case CT_NEW_WORD_SPACE_SUBSTITUTION:
-            return false;
-        case CT_INSERTION:
-            return false;
-        case CT_TRANSPOSITION:
-            return false;
-        default:
-            return false;
-    }
-}
-
 /* static */ int Weighting::getForwardInputCount(const CorrectionType correctionType) {
     switch(correctionType) {
         case CT_OMISSION:
diff --git a/native/jni/src/suggest/core/policy/weighting.h b/native/jni/src/suggest/core/policy/weighting.h
index bce479c..6e740d9 100644
--- a/native/jni/src/suggest/core/policy/weighting.h
+++ b/native/jni/src/suggest/core/policy/weighting.h
@@ -80,6 +80,10 @@
     virtual float getSpaceSubstitutionCost(const DicTraverseSession *const traverseSession,
             const DicNode *const dicNode) const = 0;
 
+    virtual ErrorType getErrorType(const CorrectionType correctionType,
+            const DicTraverseSession *const traverseSession,
+            const DicNode *const parentDicNode, const DicNode *const dicNode) const = 0;
+
     Weighting() {}
     virtual ~Weighting() {}
 
@@ -95,12 +99,6 @@
             const DicNode *const parentDicNode, const DicNode *const dicNode,
             hash_map_compat<int, int16_t> *const bigramCacheMap);
     // TODO: Move to TypingWeighting and GestureWeighting?
-    static bool isEditCorrection(const CorrectionType correctionType);
-    // TODO: Move to TypingWeighting and GestureWeighting?
-    static bool isProximityCorrection(const Weighting *const weighting,
-            const CorrectionType correctionType, const DicTraverseSession *const traverseSession,
-            const DicNode *const dicNode);
-    // TODO: Move to TypingWeighting and GestureWeighting?
     static int getForwardInputCount(const CorrectionType correctionType);
 };
 } // namespace latinime
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index 9de2cd2..4f94a9a 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -161,12 +161,15 @@
                 + doubleLetterCost;
         const TerminalAttributes terminalAttributes(traverseSession->getOffsetDict(),
                 terminalDicNode->getFlags(), terminalDicNode->getAttributesPos());
-        const int originalTerminalProbability = terminalDicNode->getProbability();
+        const bool isPossiblyOffensiveWord = terminalDicNode->getProbability() <= 0;
+        const bool isExactMatch = terminalDicNode->isExactMatch();
+        const int outputTypeFlags =
+                isPossiblyOffensiveWord ? Dictionary::KIND_FLAG_POSSIBLY_OFFENSIVE : 0
+                | isExactMatch ? Dictionary::KIND_FLAG_EXACT_MATCH : 0;
 
-        // Do not suggest words with a 0 probability, or entries that are blacklisted or do not
-        // represent a word. However, we should still submit their shortcuts if any.
-        const bool isValidWord =
-                originalTerminalProbability > 0 && !terminalAttributes.isBlacklistedOrNotAWord();
+        // Entries that are blacklisted or do not represent a word should not be output.
+        const bool isValidWord = !terminalAttributes.isBlacklistedOrNotAWord();
+
         // Increase output score of top typing suggestion to ensure autocorrection.
         // TODO: Better integration with java side autocorrection logic.
         // Force autocorrection for obvious long multi-word suggestions.
@@ -188,10 +191,9 @@
             }
         }
 
-        // Do not suggest words with a 0 probability, or entries that are blacklisted or do not
-        // represent a word. However, we should still submit their shortcuts if any.
+        // Don't output invalid words. However, we still need to submit their shortcuts if any.
         if (isValidWord) {
-            outputTypes[outputWordIndex] = Dictionary::KIND_CORRECTION;
+            outputTypes[outputWordIndex] = Dictionary::KIND_CORRECTION | outputTypeFlags;
             frequencies[outputWordIndex] = finalScore;
             // Populate the outputChars array with the suggested word.
             const int startIndex = outputWordIndex * MAX_WORD_LENGTH;
@@ -294,8 +296,8 @@
                     correctionDicNode.advanceDigraphIndex();
                     processDicNodeAsDigraph(traverseSession, &correctionDicNode);
                 }
-                if (allowsErrorCorrections
-                        && TRAVERSAL->isOmission(traverseSession, &dicNode, childDicNode)) {
+                if (TRAVERSAL->isOmission(traverseSession, &dicNode, childDicNode,
+                        allowsErrorCorrections)) {
                     // TODO: (Gesture) Change weight between omission and substitution errors
                     // TODO: (Gesture) Terminal node should not be handled as omission
                     correctionDicNode.initByCopy(childDicNode);
@@ -422,20 +424,15 @@
  */
 void Suggest::processDicNodeAsOmission(
         DicTraverseSession *traverseSession, DicNode *dicNode) const {
-    // If the omission is surely intentional that it should incur zero cost.
-    const bool isZeroCostOmission = dicNode->isZeroCostOmission();
     DicNodeVector childDicNodes;
-
     DicNodeUtils::getAllChildDicNodes(dicNode, traverseSession->getOffsetDict(), &childDicNodes);
 
     const int size = childDicNodes.getSizeAndLock();
     for (int i = 0; i < size; i++) {
         DicNode *const childDicNode = childDicNodes[i];
-        if (!isZeroCostOmission) {
-            // Treat this word as omission
-            Weighting::addCostAndForwardInputIndex(WEIGHTING, CT_OMISSION, traverseSession,
-                    dicNode, childDicNode, 0 /* bigramCacheMap */);
-        }
+        // Treat this word as omission
+        Weighting::addCostAndForwardInputIndex(WEIGHTING, CT_OMISSION, traverseSession,
+                dicNode, childDicNode, 0 /* bigramCacheMap */);
         weightChildNode(traverseSession, childDicNode);
 
         if (!TRAVERSAL->isPossibleOmissionChildNode(traverseSession, dicNode, childDicNode)) {
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
index 9f83474..fb1fb79 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
@@ -43,10 +43,17 @@
     }
 
     AK_FORCE_INLINE bool isOmission(const DicTraverseSession *const traverseSession,
-            const DicNode *const dicNode, const DicNode *const childDicNode) const {
+            const DicNode *const dicNode, const DicNode *const childDicNode,
+            const bool allowsErrorCorrections) const {
         if (!CORRECT_OMISSION) {
             return false;
         }
+        // Note: Always consider intentional omissions (like apostrophes) since they are common.
+        const bool canConsiderOmission =
+                allowsErrorCorrections || childDicNode->canBeIntentionalOmission();
+        if (!canConsiderOmission) {
+            return false;
+        }
         const int inputSize = traverseSession->getInputSize();
         // TODO: Don't refer to isCompletion?
         if (dicNode->isCompletion(inputSize)) {
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_weighting.cpp b/native/jni/src/suggest/policyimpl/typing/typing_weighting.cpp
index 1500341..47bd204 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_weighting.cpp
+++ b/native/jni/src/suggest/policyimpl/typing/typing_weighting.cpp
@@ -21,4 +21,39 @@
 
 namespace latinime {
 const TypingWeighting TypingWeighting::sInstance;
+
+ErrorType TypingWeighting::getErrorType(const CorrectionType correctionType,
+        const DicTraverseSession *const traverseSession,
+        const DicNode *const parentDicNode, const DicNode *const dicNode) const {
+    switch (correctionType) {
+        case CT_MATCH:
+            if (isProximityDicNode(traverseSession, dicNode)) {
+                return ET_PROXIMITY_CORRECTION;
+            } else {
+                return ET_NOT_AN_ERROR;
+            }
+        case CT_ADDITIONAL_PROXIMITY:
+            return ET_PROXIMITY_CORRECTION;
+        case CT_OMISSION:
+            if (parentDicNode->canBeIntentionalOmission()) {
+                return ET_INTENTIONAL_OMISSION;
+            } else {
+                return ET_EDIT_CORRECTION;
+            }
+            break;
+        case CT_SUBSTITUTION:
+        case CT_INSERTION:
+        case CT_TRANSPOSITION:
+            return ET_EDIT_CORRECTION;
+        case CT_NEW_WORD_SPACE_OMITTION:
+        case CT_NEW_WORD_SPACE_SUBSTITUTION:
+            return ET_NEW_WORD;
+        case CT_TERMINAL:
+            return ET_NOT_AN_ERROR;
+        case CT_COMPLETION:
+            return ET_COMPLETION;
+        default:
+            return ET_NOT_AN_ERROR;
+    }
+}
 }  // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
index 34d25ae..4a0bd71 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
@@ -50,13 +50,14 @@
     }
 
     float getOmissionCost(const DicNode *const parentDicNode, const DicNode *const dicNode) const {
-        bool sameCodePoint = false;
-        bool isFirstLetterOmission = false;
-        float cost = 0.0f;
-        sameCodePoint = dicNode->isSameNodeCodePoint(parentDicNode);
+        const bool isZeroCostOmission = parentDicNode->isZeroCostOmission();
+        const bool sameCodePoint = dicNode->isSameNodeCodePoint(parentDicNode);
         // If the traversal omitted the first letter then the dicNode should now be on the second.
-        isFirstLetterOmission = dicNode->getDepth() == 2;
-        if (isFirstLetterOmission) {
+        const bool isFirstLetterOmission = dicNode->getDepth() == 2;
+        float cost = 0.0f;
+        if (isZeroCostOmission) {
+            cost = 0.0f;
+        } else if (isFirstLetterOmission) {
             cost = ScoringParams::OMISSION_COST_FIRST_CHAR;
         } else {
             cost = sameCodePoint ? ScoringParams::OMISSION_COST_SAME_CHAR
@@ -156,15 +157,8 @@
 
     float getTerminalLanguageCost(const DicTraverseSession *const traverseSession,
             const DicNode *const dicNode, const float dicNodeLanguageImprobability) const {
-        const bool hasEditCount = dicNode->getEditCorrectionCount() > 0;
-        const bool isSameLength = dicNode->getDepth() == traverseSession->getInputSize();
-        const bool hasMultipleWords = dicNode->hasMultipleWords();
-        const bool hasProximityErrors = dicNode->getProximityCorrectionCount() > 0;
-        // Gesture input is always assumed to have proximity errors
-        // because the input word shouldn't be treated as perfect
-        const bool isExactMatch = !hasEditCount && !hasMultipleWords
-                && !hasProximityErrors && isSameLength;
-        const float languageImprobability = isExactMatch ? 0.0f : dicNodeLanguageImprobability;
+        const float languageImprobability = (dicNode->isExactMatch()) ?
+                0.0f : dicNodeLanguageImprobability;
         return languageImprobability * ScoringParams::DISTANCE_WEIGHT_LANGUAGE;
     }
 
@@ -189,6 +183,10 @@
         return cost * traverseSession->getMultiWordCostMultiplier();
     }
 
+    ErrorType getErrorType(const CorrectionType correctionType,
+            const DicTraverseSession *const traverseSession,
+            const DicNode *const parentDicNode, const DicNode *const dicNode) const;
+
  private:
     DISALLOW_COPY_AND_ASSIGN(TypingWeighting);
     static const TypingWeighting sInstance;