Merge "Factor dict pack settings reading into a static inner class"
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index c2200b5..172ca2f 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -107,7 +107,6 @@
     </declare-styleable>
 
     <declare-styleable name="CandidateView">
-        <attr name="autoCorrectionVisualFlashEnabled" format="boolean" />
         <attr name="autoCorrectHighlight" format="integer">
             <flag name="autoCorrectBold" value="0x01" />
             <flag name="autoCorrectUnderline" value="0x02" />
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 6327ede..1c7c1a1 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -39,7 +39,6 @@
     <bool name="config_default_compat_recorrection_enabled">true</bool>
     <bool name="config_default_sound_enabled">false</bool>
     <bool name="config_auto_correction_spacebar_led_enabled">true</bool>
-    <bool name="config_auto_correction_suggestion_strip_visual_flash_enabled">false</bool>
     <!-- Showing mini keyboard, just above the touched point if true, aligned to the key if false -->
     <bool name="config_show_mini_keyboard_at_touched_point">false</bool>
     <!-- The language is never displayed if == 0, always displayed if < 0 -->
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index a47eeed..8145d05 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -85,7 +85,6 @@
         <item name="android:background">@drawable/candidate_feedback_background</item>
     </style>
     <style name="CandidateViewStyle" parent="SuggestionsStripBackgroundStyle">
-        <item name="autoCorrectionVisualFlashEnabled">@bool/config_auto_correction_suggestion_strip_visual_flash_enabled</item>
         <item name="autoCorrectHighlight">autoCorrectBold</item>
         <item name="colorTypedWord">#FFFFFFFF</item>
         <item name="colorAutoCorrect">#FFFCAE00</item>
@@ -189,8 +188,7 @@
         <item name="android:background">@drawable/keyboard_popup_panel_background_holo</item>
     </style>
     <style name="CandidateViewStyle.IceCreamSandwich" parent="SuggestionsStripBackgroundStyle.IceCreamSandwich">
-        <item name="autoCorrectionVisualFlashEnabled">@bool/config_auto_correction_suggestion_strip_visual_flash_enabled</item>
-        <item name="autoCorrectHighlight">autoCorrectBold|autoCorrectInvert</item>
+        <item name="autoCorrectHighlight">autoCorrectBold</item>
         <item name="colorTypedWord">#FFFFFFFF</item>
         <item name="colorAutoCorrect">#FF3DC8FF</item>
         <item name="colorSuggested">#FFFFFFFF</item>
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index ae9809e..3dca9aa 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -20,12 +20,18 @@
 import android.content.SharedPreferences;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.inputmethodservice.InputMethodService;
+import android.media.AudioManager;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
 
 import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
+import com.android.inputmethod.compat.AudioManagerCompatWrapper;
+import com.android.inputmethod.compat.EditorInfoCompatUtils;
+import com.android.inputmethod.compat.InputTypeCompatUtils;
 import com.android.inputmethod.compat.MotionEventCompatUtils;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.KeyDetector;
@@ -39,17 +45,19 @@
     // Delay in milliseconds between key press DOWN and UP events
     private static final long DELAY_KEY_PRESS = 10;
 
-    private int mScaledEdgeSlop;
+    private InputMethodService mInputMethod;
+    private FlickGestureDetector mGestureDetector;
     private LatinKeyboardBaseView mView;
     private AccessibleKeyboardActionListener mListener;
-    private FlickGestureDetector mGestureDetector;
+    private AudioManagerCompatWrapper mAudioManager;
 
+    private int mScaledEdgeSlop;
     private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
     private int mLastX = -1;
     private int mLastY = -1;
 
-    public static void init(Context context, SharedPreferences prefs) {
-        sInstance.initInternal(context, prefs);
+    public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
+        sInstance.initInternal(inputMethod, prefs);
         sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
     }
 
@@ -65,15 +73,36 @@
         // Not publicly instantiable.
     }
 
-    private void initInternal(Context context, SharedPreferences prefs) {
+    private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
         final Paint paint = new Paint();
         paint.setTextAlign(Paint.Align.LEFT);
         paint.setTextSize(14.0f);
         paint.setAntiAlias(true);
         paint.setColor(Color.YELLOW);
 
-        mGestureDetector = new KeyboardFlickGestureDetector(context);
-        mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
+        mInputMethod = inputMethod;
+        mGestureDetector = new KeyboardFlickGestureDetector(inputMethod);
+        mScaledEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop();
+
+        final AudioManager audioManager = (AudioManager) inputMethod
+                .getSystemService(Context.AUDIO_SERVICE);
+        mAudioManager = new AudioManagerCompatWrapper(audioManager);
+    }
+
+    /**
+     * @return {@code true} if the device should not speak text (eg. non-control) characters
+     */
+    private boolean shouldObscureInput() {
+        // Always speak if the user is listening through headphones.
+        if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
+            return false;
+
+        final EditorInfo info = mInputMethod.getCurrentInputEditorInfo();
+        if (info == null)
+            return false;
+
+        // Don't speak if the IME is connected to a password field.
+        return InputTypeCompatUtils.isPasswordInputType(info.inputType);
     }
 
     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
@@ -90,8 +119,10 @@
             if (key == null)
                 break;
 
+            final boolean shouldObscure = shouldObscureInput();
             final CharSequence description = KeyCodeDescriptionMapper.getInstance()
-                    .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key);
+                    .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key,
+                            shouldObscure);
 
             if (description == null)
                 return false;
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index ec4287d..7302830 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -28,6 +28,9 @@
 import java.util.HashMap;
 
 public class KeyCodeDescriptionMapper {
+    // The resource ID of the string spoken for obscured keys
+    private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
+
     private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
 
     // Map of key labels to spoken description resource IDs
@@ -118,10 +121,12 @@
      * @param context The package's context.
      * @param keyboard The keyboard on which the key resides.
      * @param key The key from which to obtain a description.
+     * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
      * @return a character sequence describing the action performed by pressing
      *         the key
      */
-    public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key) {
+    public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key,
+            boolean shouldObscure) {
         if (key.mCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
             final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
             if (description != null)
@@ -136,12 +141,12 @@
             } else if (label.length() == 1
                     || (keyboard.isManualTemporaryUpperCase() && !TextUtils
                             .isEmpty(key.mHintLabel))) {
-                return getDescriptionForKeyCode(context, keyboard, key);
+                return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
             } else {
                 return label;
             }
         } else if (key.mCode != Keyboard.CODE_DUMMY) {
-            return getDescriptionForKeyCode(context, keyboard, key);
+            return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
         }
 
         return null;
@@ -206,19 +211,29 @@
      * @param context The package's context.
      * @param keyboard The keyboard on which the key resides.
      * @param key The key from which to obtain a description.
+     * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
      * @return a character sequence describing the action performed by pressing
      *         the key
      */
-    private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key) {
+    private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key,
+            boolean shouldObscure) {
         final int code = getCorrectKeyCode(keyboard, key);
 
         if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) {
             return context.getString(mShiftLockedKeyCodeMap.get(code));
         } else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(code)) {
             return context.getString(mShiftedKeyCodeMap.get(code));
-        } else if (mKeyCodeMap.containsKey(code)) {
+        }
+
+        // If the key description should be obscured, now is the time to do it.
+        final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code);
+        if (shouldObscure && isDefinedNonCtrl) {
+            return context.getString(OBSCURED_KEY_RES_ID);
+        }
+
+        if (mKeyCodeMap.containsKey(code)) {
             return context.getString(mKeyCodeMap.get(code));
-        } else if (Character.isDefined(code) && !Character.isISOControl(code)) {
+        } else if (isDefinedNonCtrl) {
             return Character.toString((char) code);
         } else {
             return context.getString(R.string.spoken_description_unknown, code);
diff --git a/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
new file mode 100644
index 0000000..b6c3e2a
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.compat;
+
+import android.media.AudioManager;
+
+import java.lang.reflect.Method;
+
+public class AudioManagerCompatWrapper {
+    private static final Method METHOD_isWiredHeadsetOn = CompatUtils.getMethod(
+            AudioManager.class, "isWiredHeadsetOn");
+    private static final Method METHOD_isBluetoothA2dpOn = CompatUtils.getMethod(
+            AudioManager.class, "isBluetoothA2dpOn");
+
+    private final AudioManager mManager;
+
+    public AudioManagerCompatWrapper(AudioManager manager) {
+        mManager = manager;
+    }
+
+    /**
+     * Checks whether audio routing to the wired headset is on or off.
+     *
+     * @return true if audio is being routed to/from wired headset;
+     *         false if otherwise
+     */
+    public boolean isWiredHeadsetOn() {
+        return (Boolean) CompatUtils.invoke(mManager, false, METHOD_isWiredHeadsetOn);
+    }
+
+    /**
+     * Checks whether A2DP audio routing to the Bluetooth headset is on or off.
+     *
+     * @return true if A2DP audio is being routed to/from Bluetooth headset;
+     *         false if otherwise
+     */
+    public boolean isBluetoothA2dpOn() {
+        return (Boolean) CompatUtils.invoke(mManager, false, METHOD_isBluetoothA2dpOn);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
index 0640fd0..f499bc0 100644
--- a/java/src/com/android/inputmethod/latin/CandidateView.java
+++ b/java/src/com/android/inputmethod/latin/CandidateView.java
@@ -278,7 +278,6 @@
 
         private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
 
-        public final boolean mAutoCorrectionVisualFlashEnabled;
         public boolean mMoreSuggestionsAvailable;
 
         public SuggestionsStripParams(Context context, AttributeSet attrs, int defStyle,
@@ -286,8 +285,6 @@
             super(words, dividers, infos);
             final TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle);
-            mAutoCorrectionVisualFlashEnabled = a.getBoolean(
-                    R.styleable.CandidateView_autoCorrectionVisualFlashEnabled, false);
             mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0);
             mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0);
             mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0);
@@ -584,7 +581,7 @@
         final int width = getWidth();
         final int countInStrip = mStripParams.layout(
                 mSuggestions, mCandidatesStrip, mCandidatesPane, width);
-        final int countInPane = mPaneParams.layout(
+        mPaneParams.layout(
                 mSuggestions, mCandidatesPane, countInStrip, mStripParams.getTextColor(), width);
     }
 
@@ -633,6 +630,7 @@
 
     private static CharSequence getEllipsizedText(CharSequence text, int maxWidth,
             TextPaint paint) {
+        if (text == null) return null;
         paint.setTextScaleX(1.0f);
         final int width = getTextWidth(text, paint);
         if (width <= maxWidth) {
@@ -703,9 +701,6 @@
     }
 
     public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) {
-        if (!mStripParams.mAutoCorrectionVisualFlashEnabled) {
-            return;
-        }
         final CharSequence inverted = mStripParams.getInvertedText(autoCorrectedWord);
         if (inverted == null)
             return;
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index c718410..649774d 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin.spellcheck;
 
+import android.content.Intent;
 import android.content.res.Resources;
 import android.service.textservice.SpellCheckerService;
 import android.service.textservice.SpellCheckerService.Session;
@@ -48,7 +49,7 @@
     private static final int POOL_SIZE = 2;
 
     private final static String[] emptyArray = new String[0];
-    private final Map<String, DictionaryPool> mDictionaryPools =
+    private Map<String, DictionaryPool> mDictionaryPools =
             Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
 
     @Override
@@ -104,6 +105,16 @@
         }
     }
 
+    @Override
+    public boolean onUnbind(final Intent intent) {
+        final Map<String, DictionaryPool> oldPools = mDictionaryPools;
+        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
+        for (DictionaryPool pool : oldPools.values()) {
+            pool.close();
+        }
+        return false;
+    }
+
     private DictionaryPool getDictionaryPool(final String locale) {
         DictionaryPool pool = mDictionaryPools.get(locale);
         if (null == pool) {
@@ -167,7 +178,9 @@
                 dictInfo.mDictionary.getWords(composer, suggestionsGatherer,
                         dictInfo.mProximityInfo);
                 isInDict = dictInfo.mDictionary.isValidWord(text);
-                mDictionaryPool.offer(dictInfo);
+                if (!mDictionaryPool.offer(dictInfo)) {
+                    Log.e(TAG, "Can't re-insert a dictionary into its pool");
+                }
             } catch (InterruptedException e) {
                 // I don't think this can happen.
                 return new SuggestionsInfo(0, new String[0]);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index dfbfcc7..ee294f6 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -28,7 +28,8 @@
     private final AndroidSpellCheckerService mService;
     private final int mMaxSize;
     private final Locale mLocale;
-    private int mSize = 0;
+    private int mSize;
+    private volatile boolean mClosed;
 
     public DictionaryPool(final int maxSize, final AndroidSpellCheckerService service,
             final Locale locale) {
@@ -36,6 +37,8 @@
         mMaxSize = maxSize;
         mService = service;
         mLocale = locale;
+        mSize = 0;
+        mClosed = false;
     }
 
     @Override
@@ -52,4 +55,24 @@
             }
         }
     }
+
+    public void close() {
+        synchronized(this) {
+            mClosed = true;
+            for (DictAndProximity dict : this) {
+                dict.mDictionary.close();
+            }
+            clear();
+        }
+    }
+
+    @Override
+    public boolean offer(final DictAndProximity dict) {
+        if (mClosed) {
+            dict.mDictionary.close();
+            return false;
+        } else {
+            return super.offer(dict);
+        }
+    }
 }
diff --git a/native/src/correction.cpp b/native/src/correction.cpp
index a4090a9..99412b2 100644
--- a/native/src/correction.cpp
+++ b/native/src/correction.cpp
@@ -95,10 +95,8 @@
     }
 
     *word = mWord;
-    const bool sameLength = (mExcessivePos == mInputLength - 1) ? (mInputLength == inputIndex + 2)
-            : (mInputLength == inputIndex + 1);
     return Correction::RankingAlgorithm::calculateFinalFreq(
-            inputIndex, outputIndex, freq, sameLength, mEditDistanceTable, this);
+            inputIndex, outputIndex, freq, mEditDistanceTable, this);
 }
 
 bool Correction::initProcessState(const int outputIndex) {
@@ -205,20 +203,6 @@
     }
 
     if (mNeedsToTraverseAllNodes || isQuote(c)) {
-        const bool checkProximityChars =
-                !(mSkippedCount > 0 || mExcessivePos >= 0 || mTransposedPos >= 0);
-        // Note: This logic tries saving cases like contrst --> contrast -- "a" is one of
-        // proximity chars of "s", but it should rather be handled as a skipped char.
-        if (checkProximityChars
-                && mInputIndex > 0
-                && mCorrectionStates[mOutputIndex].mProximityMatching
-                && mCorrectionStates[mOutputIndex].mSkipping
-                && mProximityInfo->getMatchedProximityId(
-                        mInputIndex - 1, c, false)
-                        == ProximityInfo::SAME_OR_ACCENTED_OR_CAPITALIZED_CHAR) {
-            ++mSkippedCount;
-            --mProximityCount;
-        }
         return processSkipChar(c, isTerminal);
     } else {
         int inputIndexForProximity = mInputIndex;
@@ -250,6 +234,8 @@
                     && mProximityInfo->getMatchedProximityId(
                             inputIndexForProximity - 1, c, false)
                                     == ProximityInfo::SAME_OR_ACCENTED_OR_CAPITALIZED_CHAR) {
+                // Note: This logic tries saving cases like contrst --> contrast -- "a" is one of
+                // proximity chars of "s", but it should rather be handled as a skipped char.
                 ++mSkippedCount;
                 --mProximityCount;
                 return processSkipChar(c, isTerminal);
@@ -344,6 +330,16 @@
     }
 }
 
+inline static int getQuoteCount(const unsigned short* word, const int length) {
+    int quoteCount = 0;
+    for (int i = 0; i < length; ++i) {
+        if(word[i] == '\'') {
+            ++quoteCount;
+        }
+    }
+    return quoteCount;
+}
+
 /* static */
 inline static int editDistance(
         int* editDistanceTable, const unsigned short* input,
@@ -392,8 +388,7 @@
 
 /* static */
 int Correction::RankingAlgorithm::calculateFinalFreq(const int inputIndex, const int outputIndex,
-        const int freq, const bool sameLength, int* editDistanceTable,
-        const Correction* correction) {
+        const int freq, int* editDistanceTable, const Correction* correction) {
     const int excessivePos = correction->getExcessivePos();
     const int transposedPos = correction->getTransposedPos();
     const int inputLength = correction->mInputLength;
@@ -402,6 +397,12 @@
     const ProximityInfo *proximityInfo = correction->mProximityInfo;
     const int skipCount = correction->mSkippedCount;
     const int proximityMatchedCount = correction->mProximityCount;
+    if (skipCount >= inputLength || inputLength == 0) {
+        return -1;
+    }
+    const bool sameLength = (excessivePos == inputLength - 1) ? (inputLength == inputIndex + 2)
+            : (inputLength == inputIndex + 1);
+
 
     // TODO: use mExcessiveCount
     int matchCount = inputLength - correction->mProximityCount - (excessivePos >= 0 ? 1 : 0);
@@ -409,67 +410,52 @@
     const unsigned short* word = correction->mWord;
     const bool skipped = skipCount > 0;
 
-    // ----- TODO: use edit distance here as follows? ---------------------- /
-    //if (!skipped && excessivePos < 0 && transposedPos < 0) {
-    //    const int ed = editDistance(dp, proximityInfo->getInputWord(),
-    //            inputLength, word, outputIndex + 1);
-    //    matchCount = outputIndex + 1 - ed;
-    //    if (ed == 1 && !sameLength) ++matchCount;
-    //}
-    //    const int ed = editDistance(dp, proximityInfo->getInputWord(),
-    //    inputLength, word, outputIndex + 1);
-    //    if (ed == 1 && !sameLength) ++matchCount; ------------------------ /
-    int matchWeight = powerIntCapped(typedLetterMultiplier, matchCount);
+    const int quoteDiffCount = max(0, getQuoteCount(word, outputIndex + 1)
+            - getQuoteCount(proximityInfo->getPrimaryInputWord(), inputLength));
+
+    // TODO: Calculate edit distance for transposed and excessive
+    int matchWeight;
+    int ed = 0;
+    int adJustedProximityMatchedCount = proximityMatchedCount;
+    if (excessivePos < 0 && transposedPos < 0 && (proximityMatchedCount > 0 || skipped)) {
+        const unsigned short* primaryInputWord = proximityInfo->getPrimaryInputWord();
+        ed = editDistance(editDistanceTable, primaryInputWord,
+                inputLength, word, outputIndex + 1);
+        matchWeight = powerIntCapped(typedLetterMultiplier, outputIndex + 1 - ed);
+        if (ed == 1 && inputLength == outputIndex) {
+            // Promote a word with just one skipped char
+            multiplyRate(WORDS_WITH_JUST_ONE_CORRECTION_PROMOTION_RATE, &matchWeight);
+        }
+        ed = max(0, ed - quoteDiffCount);
+        adJustedProximityMatchedCount = min(max(0, ed - (outputIndex + 1 - inputLength)),
+                proximityMatchedCount);
+    } else {
+        matchWeight = powerIntCapped(typedLetterMultiplier, matchCount);
+    }
 
     // TODO: Demote by edit distance
     int finalFreq = freq * matchWeight;
-    // +1 +11/-12
-    /*if (inputLength == outputIndex && !skipped && excessivePos < 0 && transposedPos < 0) {
-        const int ed = editDistance(dp, proximityInfo->getInputWord(),
-                inputLength, word, outputIndex + 1);
-        if (ed == 1) {
-            multiplyRate(160, &finalFreq);
-        }
-    }*/
-    if (inputLength == outputIndex && excessivePos < 0 && transposedPos < 0
-            && (proximityMatchedCount > 0 || skipped)) {
-        const int ed = editDistance(editDistanceTable, proximityInfo->getPrimaryInputWord(),
-                inputLength, word, outputIndex + 1);
-        if (ed == 1) {
-            multiplyRate(160, &finalFreq);
-        }
-    }
 
-    // TODO: Promote properly?
-    //if (skipCount == 1 && excessivePos < 0 && transposedPos < 0 && inputLength == outputIndex
-    //        && !sameLength) {
-    //    multiplyRate(150, &finalFreq);
-    //}
-    //if (skipCount == 0 && excessivePos < 0 && transposedPos < 0 && inputLength == outputIndex
-    //        && !sameLength) {
-    //    multiplyRate(150, &finalFreq);
-    //}
-    //if (skipCount == 0 && excessivePos < 0 && transposedPos < 0
-    //        && inputLength == outputIndex + 1) {
-    //    multiplyRate(150, &finalFreq);
-    //}
+    ///////////////////////////////////////////////
+    // Promotion and Demotion for each correction
 
+    // Demotion for a word with missing character
     if (skipped) {
-        if (inputLength >= 2) {
-            const int demotionRate = WORDS_WITH_MISSING_CHARACTER_DEMOTION_RATE
-                    * (10 * inputLength - WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X)
-                    / (10 * inputLength
-                            - WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X + 10);
-            if (DEBUG_DICT_FULL) {
-                LOGI("Demotion rate for missing character is %d.", demotionRate);
-            }
-            multiplyRate(demotionRate, &finalFreq);
-        } else {
-            finalFreq = 0;
+        const int demotionRate = WORDS_WITH_MISSING_CHARACTER_DEMOTION_RATE
+                * (10 * inputLength - WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X)
+                / (10 * inputLength
+                        - WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X + 10);
+        if (DEBUG_DICT_FULL) {
+            LOGI("Demotion rate for missing character is %d.", demotionRate);
         }
+        multiplyRate(demotionRate, &finalFreq);
     }
+
+    // Demotion for a word with transposed character
     if (transposedPos >= 0) multiplyRate(
             WORDS_WITH_TRANSPOSED_CHARACTERS_DEMOTION_RATE, &finalFreq);
+
+    // Demotion for a word with excessive character
     if (excessivePos >= 0) {
         multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_DEMOTION_RATE, &finalFreq);
         if (!proximityInfo->existsAdjacentProximityChars(inputIndex)) {
@@ -478,52 +464,62 @@
             multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_OUT_OF_PROXIMITY_DEMOTION_RATE, &finalFreq);
         }
     }
-    int lengthFreq = typedLetterMultiplier;
-    multiplyIntCapped(powerIntCapped(typedLetterMultiplier, outputIndex), &lengthFreq);
-    if ((outputIndex + 1) == matchCount) {
-        // Full exact match
-        if (outputIndex > 1) {
-            if (DEBUG_DICT) {
-                LOGI("Found full matched word.");
-            }
-            multiplyRate(FULL_MATCHED_WORDS_PROMOTION_RATE, &finalFreq);
-        }
-        if (sameLength && transposedPos < 0 && !skipped && excessivePos < 0) {
-            finalFreq = capped255MultForFullMatchAccentsOrCapitalizationDifference(finalFreq);
-        }
-    } else if (sameLength && transposedPos < 0 && !skipped && excessivePos < 0
-            && outputIndex > 0) {
+
+    // Promotion for a word with proximity characters
+    for (int i = 0; i < adJustedProximityMatchedCount; ++i) {
         // A word with proximity corrections
-        if (DEBUG_DICT) {
-            LOGI("Found one proximity correction.");
+        if (DEBUG_DICT_FULL) {
+            LOGI("Found a proximity correction.");
         }
         multiplyIntCapped(typedLetterMultiplier, &finalFreq);
         multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq);
     }
-    if (DEBUG_DICT_FULL) {
-        LOGI("calc: %d, %d", outputIndex, sameLength);
-    }
-    if (sameLength) multiplyIntCapped(fullWordMultiplier, &finalFreq);
 
-    // TODO: check excessive count and transposed count
+    const int errorCount = proximityMatchedCount + skipCount;
+    multiplyRate(
+            100 - CORRECTION_COUNT_RATE_DEMOTION_RATE_BASE * errorCount / inputLength, &finalFreq);
+
+    // Promotion for an exactly matched word
+    if (matchCount == outputIndex + 1) {
+        // Full exact match
+        if (sameLength && transposedPos < 0 && !skipped && excessivePos < 0) {
+            finalFreq = capped255MultForFullMatchAccentsOrCapitalizationDifference(finalFreq);
+        }
+    }
+
+    // Promote a word with no correction
+    if (proximityMatchedCount == 0 && transposedPos < 0 && !skipped && excessivePos < 0) {
+        multiplyRate(FULL_MATCHED_WORDS_PROMOTION_RATE, &finalFreq);
+    }
+
+    // TODO: Check excessive count and transposed count
+    // TODO: Remove this if possible
     /*
-     If the last character of the user input word is the same as the next character
-     of the output word, and also all of characters of the user input are matched
-     to the output word, we'll promote that word a bit because
-     that word can be considered the combination of skipped and matched characters.
-     This means that the 'sm' pattern wins over the 'ma' pattern.
-     e.g.)
-     shel -> shell [mmmma] or [mmmsm]
-     hel -> hello [mmmaa] or [mmsma]
-     m ... matching
-     s ... skipping
-     a ... traversing all
+         If the last character of the user input word is the same as the next character
+         of the output word, and also all of characters of the user input are matched
+         to the output word, we'll promote that word a bit because
+         that word can be considered the combination of skipped and matched characters.
+         This means that the 'sm' pattern wins over the 'ma' pattern.
+         e.g.)
+         shel -> shell [mmmma] or [mmmsm]
+         hel -> hello [mmmaa] or [mmsma]
+         m ... matching
+         s ... skipping
+         a ... traversing all
      */
     if (matchCount == inputLength && matchCount >= 2 && !skipped
             && word[matchCount] == word[matchCount - 1]) {
         multiplyRate(WORDS_WITH_MATCH_SKIP_PROMOTION_RATE, &finalFreq);
     }
 
+    if (sameLength) {
+        multiplyIntCapped(fullWordMultiplier, &finalFreq);
+    }
+
+    if (DEBUG_DICT_FULL) {
+        LOGI("calc: %d, %d", outputIndex, sameLength);
+    }
+
     return finalFreq;
 }
 
diff --git a/native/src/correction.h b/native/src/correction.h
index 9d385a4..871a042 100644
--- a/native/src/correction.h
+++ b/native/src/correction.h
@@ -139,8 +139,7 @@
     class RankingAlgorithm {
     public:
         static int calculateFinalFreq(const int inputIndex, const int depth,
-                const int freq, const bool sameLength, int *editDistanceTable,
-                const Correction* correction);
+                const int freq, int *editDistanceTable, const Correction* correction);
         static int calcFreqForSplitTwoWords(const int firstFreq, const int secondFreq,
                 const Correction* correction);
     };
diff --git a/native/src/defines.h b/native/src/defines.h
index c1d08e6..a29fb7e 100644
--- a/native/src/defines.h
+++ b/native/src/defines.h
@@ -177,6 +177,8 @@
 #define FULL_MATCHED_WORDS_PROMOTION_RATE 120
 #define WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE 90
 #define WORDS_WITH_MATCH_SKIP_PROMOTION_RATE 105
+#define WORDS_WITH_JUST_ONE_CORRECTION_PROMOTION_RATE 160
+#define CORRECTION_COUNT_RATE_DEMOTION_RATE_BASE 42
 
 // This should be greater than or equal to MAX_WORD_LENGTH defined in BinaryDictionary.java
 // This is only used for the size of array. Not to be used in c functions.
@@ -194,5 +196,6 @@
 #define MIN_USER_TYPED_LENGTH_FOR_EXCESSIVE_CHARACTER_SUGGESTION 3
 
 #define min(a,b) ((a)<(b)?(a):(b))
+#define max(a,b) ((a)>(b)?(a):(b))
 
 #endif // LATINIME_DEFINES_H