Merge "Move the auto correction functionalities to AutoCorrection.java"
diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml
index b1f7379..8dec7ab 100644
--- a/java/res/xml/method.xml
+++ b/java/res/xml/method.xml
@@ -65,6 +65,7 @@
             android:label="@string/subtype_mode_de_keyboard"
             android:imeSubtypeLocale="de"
             android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="requiresGermanUmlautProcessing"
     />
     <subtype android:icon="@drawable/ic_subtype_mic"
             android:label="@string/subtype_mode_de_voice"
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardParser.java b/java/src/com/android/inputmethod/keyboard/KeyboardParser.java
index 2632796..feb56ab 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardParser.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardParser.java
@@ -447,7 +447,8 @@
                     textAttr(KeyboardId.modeName(
                             a.getInt(R.styleable.Keyboard_Case_mode, -1)), "mode"),
                     textAttr(KeyboardId.colorSchemeName(
-                            a.getInt(R.styleable.KeyboardView_colorScheme, -1)), "colorSchemeName"),
+                            viewAttr.getInt(
+                                    R.styleable.KeyboardView_colorScheme, -1)), "colorSchemeName"),
                     booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"),
                     booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"),
                     booleanAttr(a, R.styleable.Keyboard_Case_voiceKeyEnabled, "voiceKeyEnabled"),
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 3d34e71..64a23ab 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -194,7 +194,7 @@
         // displayed on its spacebar, it might have had arbitrary text fade factor. In such case,
         // we should reset the text fade factor. It is also applicable to shortcut key.
         keyboard.setSpacebarTextFadeFactor(0.0f, null);
-        keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutAvailable(), null);
+        keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady(), null);
         return keyboard;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index d866ec1..08ddd25 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -58,6 +58,25 @@
     private final int[] mFrequencies_bigrams = new int[MAX_BIGRAMS];
 
     private final KeyboardSwitcher mKeyboardSwitcher = KeyboardSwitcher.getInstance();
+    private final SubtypeSwitcher mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+
+    private static class Flags {
+        private static class FlagEntry {
+            public final String mName;
+            public final int mValue;
+            public FlagEntry(String name, int value) {
+                mName = name;
+                mValue = value;
+            }
+        }
+        public static final FlagEntry[] ALL_FLAGS = {
+            // Here should reside all flags that trigger some special processing
+            // These *must* match the definition in UnigramDictionary enum in
+            // unigram_dictionary.h so please update both at the same time.
+            new FlagEntry("requiresGermanUmlautProcessing", 0x1)
+        };
+    }
+    private int mFlags = 0;
 
     private BinaryDictionary() {
     }
@@ -91,6 +110,7 @@
                 return null;
             }
         }
+        sInstance.initFlags();
         return sInstance;
     }
 
@@ -109,16 +129,26 @@
         return sInstance;
     }
 
+    private void initFlags() {
+        int flags = 0;
+        for (Flags.FlagEntry entry : Flags.ALL_FLAGS) {
+            if (mSubtypeSwitcher.currentSubtypeContainsExtraValueKey(entry.mName))
+                flags |= entry.mValue;
+        }
+        mFlags = flags;
+    }
+
     static {
         Utils.loadNativeLibrary();
     }
+
     private native int openNative(String sourceDir, long dictOffset, long dictSize,
             int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength,
             int maxWords, int maxAlternatives);
     private native void closeNative(int dict);
     private native boolean isValidWordNative(int nativeData, char[] word, int wordLength);
     private native int getSuggestionsNative(int dict, int proximityInfo, int[] xCoordinates,
-            int[] yCoordinates, int[] inputCodes, int codesSize, char[] outputChars,
+            int[] yCoordinates, int[] inputCodes, int codesSize, int flags, char[] outputChars,
             int[] frequencies);
     private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength,
             int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies,
@@ -207,7 +237,7 @@
         return getSuggestionsNative(
                 mNativeDict, keyboard.getProximityInfo(),
                 codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize,
-                outputChars, frequencies);
+                mFlags, outputChars, frequencies);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
index 9699ad1..5719b90 100644
--- a/java/src/com/android/inputmethod/latin/CandidateView.java
+++ b/java/src/com/android/inputmethod/latin/CandidateView.java
@@ -347,9 +347,6 @@
         if (mShowingAddToDictionary && index == 0) {
             addToDictionary(word);
         } else {
-            if (!mSuggestions.mIsApplicationSpecifiedCompletions) {
-                TextEntryState.acceptedSuggestion(mSuggestions.getWord(0), word);
-            }
             mService.pickSuggestionManually(index, word);
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 611d362..646de66 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -542,12 +542,13 @@
 
         mSubtypeSwitcher.updateParametersOnStartInputView();
 
-        TextEntryState.newSession(this);
+        TextEntryState.reset();
 
         // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to
         // know now whether this is a password text field, because we need to know now whether we
         // want to enable the voice button.
-        mVoiceConnector.resetVoiceStates(Utils.isPasswordInputType(attribute.inputType)
+        final VoiceIMEConnector voiceIme = mVoiceConnector;
+        voiceIme.resetVoiceStates(Utils.isPasswordInputType(attribute.inputType)
                 || Utils.isVisiblePasswordInputType(attribute.inputType));
 
         initializeInputAttributes(attribute);
@@ -562,8 +563,8 @@
         loadSettings(attribute);
         if (mSubtypeSwitcher.isKeyboardMode()) {
             switcher.loadKeyboard(attribute,
-                    mVoiceConnector.isVoiceButtonEnabled(),
-                    mVoiceConnector.isVoiceButtonOnPrimary());
+                    mSubtypeSwitcher.isShortcutImeEnabled() && voiceIme.isVoiceButtonEnabled(),
+                    voiceIme.isVoiceButtonOnPrimary());
             switcher.updateShiftState();
         }
 
@@ -583,7 +584,7 @@
         checkRecorrectionOnStart();
         inputView.setForeground(true);
 
-        mVoiceConnector.onStartInputView(inputView.getWindowToken());
+        voiceIme.onStartInputView(inputView.getWindowToken());
 
         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
     }
@@ -746,15 +747,10 @@
             }
             mVoiceConnector.setVoiceInputHighlighted(false);
         } else if (!mHasValidSuggestions && !mJustAccepted) {
-            switch (TextEntryState.getState()) {
-            case ACCEPTED_DEFAULT:
-                TextEntryState.reset();
-                // $FALL-THROUGH$
-            case SPACE_AFTER_PICKED:
+            if (TextEntryState.isAcceptedDefault() || TextEntryState.isSpaceAfterPicked()) {
+                if (TextEntryState.isAcceptedDefault())
+                    TextEntryState.reset();
                 mJustAddedAutoSpace = false; // The user moved the cursor.
-                break;
-            default:
-                break;
             }
         }
         mJustAccepted = false;
@@ -832,7 +828,6 @@
         mVoiceConnector.hideVoiceWindow(mConfigurationChanging);
         mWordHistory.clear();
         super.hideWindow();
-        TextEntryState.endSession();
     }
 
     @Override
@@ -1231,7 +1226,7 @@
         mHandler.postUpdateShiftKeyState();
 
         TextEntryState.backspace();
-        if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) {
+        if (TextEntryState.isUndoCommit()) {
             revertLastWord(deleteChar);
             ic.endBatchEdit();
             return;
@@ -1391,14 +1386,12 @@
 
         // Handle the case of ". ." -> " .." with auto-space if necessary
         // before changing the TextEntryState.
-        if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
-                && primaryCode == Keyboard.CODE_PERIOD) {
+        if (TextEntryState.isPunctuationAfterAccepted() && primaryCode == Keyboard.CODE_PERIOD) {
             reswapPeriodAndSpace();
         }
 
         TextEntryState.typedCharacter((char) primaryCode, true);
-        if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
-                && primaryCode != Keyboard.CODE_ENTER) {
+        if (TextEntryState.isPunctuationAfterAccepted() && primaryCode != Keyboard.CODE_ENTER) {
             swapPunctuationAndSpace();
         } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
             doubleSpace();
@@ -1430,7 +1423,6 @@
         LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
         if (inputView != null)
             inputView.closing();
-        TextEntryState.endSession();
     }
 
     private void saveWordInHistory(CharSequence result) {
@@ -1939,7 +1931,8 @@
         }
         // Reload keyboard because the current language has been changed.
         mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(),
-                mVoiceConnector.isVoiceButtonEnabled(), mVoiceConnector.isVoiceButtonOnPrimary());
+                mSubtypeSwitcher.isShortcutImeEnabled() && mVoiceConnector.isVoiceButtonEnabled(),
+                mVoiceConnector.isVoiceButtonOnPrimary());
         initSuggest();
         mKeyboardSwitcher.updateShiftState();
     }
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index c43f5b5..dc14d77 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -46,7 +46,7 @@
 
 public class SubtypeSwitcher {
     private static boolean DBG = LatinImeLogger.sDBG;
-    private static final String TAG = "SubtypeSwitcher";
+    private static final String TAG = SubtypeSwitcher.class.getSimpleName();
 
     private static final char LOCALE_SEPARATER = '_';
     private static final String KEYBOARD_MODE = "keyboard";
@@ -74,10 +74,10 @@
     private InputMethodInfo mShortcutInputMethodInfo;
     private InputMethodSubtype mShortcutSubtype;
     private List<InputMethodSubtype> mAllEnabledSubtypesOfCurrentInputMethod;
+    private InputMethodSubtype mCurrentSubtype;
     private Locale mSystemLocale;
     private Locale mInputLocale;
     private String mInputLocaleStr;
-    private String mMode;
     private VoiceInput mVoiceInput;
     /*-----------------------------------------------------------*/
 
@@ -110,8 +110,7 @@
         mSystemLocale = null;
         mInputLocale = null;
         mInputLocaleStr = null;
-        // Mode is initialized to KEYBOARD_MODE, in case that LatinIME can't obtain currentSubtype
-        mMode = KEYBOARD_MODE;
+        mCurrentSubtype = null;
         mAllEnabledSubtypesOfCurrentInputMethod = null;
         // TODO: Voice input should be created here
         mVoiceInput = null;
@@ -145,6 +144,7 @@
 
     // Reload enabledSubtypes from the framework.
     private void updateEnabledSubtypes() {
+        final String currentMode = getCurrentSubtypeMode();
         boolean foundCurrentSubtypeBecameDisabled = true;
         mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList(
                 null, true);
@@ -157,7 +157,7 @@
             if (mLocaleSplitter.hasNext()) {
                 mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next());
             }
-            if (locale.equals(mInputLocaleStr) && mode.equals(mMode)) {
+            if (locale.equals(mInputLocaleStr) && mode.equals(currentMode)) {
                 foundCurrentSubtypeBecameDisabled = false;
             }
             if (KEYBOARD_MODE.equals(ims.getMode())) {
@@ -168,7 +168,7 @@
                 && mIsSystemLanguageSameAsInputLanguage);
         if (foundCurrentSubtypeBecameDisabled) {
             if (DBG) {
-                Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + mMode);
+                Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + currentMode);
                 Log.w(TAG, "Last subtype was disabled. Update to the current one.");
             }
             updateSubtype(mImm.getCurrentInputMethodSubtype());
@@ -209,9 +209,10 @@
     public void updateSubtype(InputMethodSubtype newSubtype) {
         final String newLocale;
         final String newMode;
+        final String oldMode = getCurrentSubtypeMode();
         if (newSubtype == null) {
             // Normally, newSubtype shouldn't be null. But just in case newSubtype was null,
-            // fallback to the default locale and mode.
+            // fallback to the default locale.
             Log.w(TAG, "Couldn't get the current subtype.");
             newLocale = "en_US";
             newMode = KEYBOARD_MODE;
@@ -221,7 +222,7 @@
         }
         if (DBG) {
             Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode
-                    + ", from: " + mInputLocaleStr + ", " + mMode);
+                    + ", from: " + mInputLocaleStr + ", " + oldMode);
         }
         boolean languageChanged = false;
         if (!newLocale.equals(mInputLocaleStr)) {
@@ -231,13 +232,12 @@
             updateInputLocale(newLocale);
         }
         boolean modeChanged = false;
-        String oldMode = mMode;
-        if (!newMode.equals(mMode)) {
-            if (mMode != null) {
+        if (!newMode.equals(oldMode)) {
+            if (oldMode != null) {
                 modeChanged = true;
             }
-            mMode = newMode;
         }
+        mCurrentSubtype = newSubtype;
 
         // If the old mode is voice input, we need to reset or cancel its status.
         // We cancel its status when we change mode, while we reset otherwise.
@@ -262,7 +262,7 @@
                 triggerVoiceIME();
             }
         } else {
-            Log.w(TAG, "Unknown subtype mode: " + mMode);
+            Log.w(TAG, "Unknown subtype mode: " + newMode);
             if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) {
                 // We need to reset the voice input to release the resources and to reset its status
                 // as it is not the current input mode.
@@ -355,11 +355,27 @@
         return false;
     }
 
-    public boolean isShortcutAvailable() {
+    public boolean isShortcutImeEnabled() {
         if (mShortcutInputMethodInfo == null)
             return false;
-        if (mShortcutSubtype != null && contains(mShortcutSubtype.getExtraValue().split(","),
-                    SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) {
+        if (mShortcutSubtype == null)
+            return true;
+        final boolean allowsImplicitlySelectedSubtypes = true;
+        for (final InputMethodSubtype enabledSubtype : mImm.getEnabledInputMethodSubtypeList(
+                mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) {
+            if (enabledSubtype.equals(mShortcutSubtype))
+                return true;
+        }
+        return false;
+    }
+
+    public boolean isShortcutImeReady() {
+        if (mShortcutInputMethodInfo == null)
+            return false;
+        if (mShortcutSubtype == null)
+            return true;
+        if (contains(mShortcutSubtype.getExtraValue().split(","),
+                SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) {
             return mIsNetworkConnected;
         }
         return true;
@@ -373,7 +389,7 @@
         final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
         final LatinKeyboard keyboard = switcher.getLatinKeyboard();
         if (keyboard != null) {
-            keyboard.updateShortcutKey(isShortcutAvailable(), switcher.getInputView());
+            keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getInputView());
         }
     }
 
@@ -483,7 +499,7 @@
     }
 
     public boolean isKeyboardMode() {
-        return KEYBOARD_MODE.equals(mMode);
+        return KEYBOARD_MODE.equals(getCurrentSubtypeMode());
     }
 
 
@@ -506,7 +522,7 @@
     }
 
     public boolean isVoiceMode() {
-        return VOICE_MODE.equals(mMode);
+        return null == mCurrentSubtype ? false : VOICE_MODE.equals(getCurrentSubtypeMode());
     }
 
     private void triggerVoiceIME() {
@@ -572,6 +588,30 @@
         }
     }
 
+    /////////////////////////////
+    // Other utility functions //
+    /////////////////////////////
+
+    public String getCurrentSubtypeExtraValue() {
+        // If null, return what an empty ExtraValue would return : the empty string.
+        return null != mCurrentSubtype ? mCurrentSubtype.getExtraValue() : "";
+    }
+
+    public boolean currentSubtypeContainsExtraValueKey(String key) {
+        // If null, return what an empty ExtraValue would return : false.
+        return null != mCurrentSubtype ? mCurrentSubtype.containsExtraValueKey(key) : false;
+    }
+
+    public String getCurrentSubtypeExtraValueOf(String key) {
+        // If null, return what an empty ExtraValue would return : null.
+        return null != mCurrentSubtype ? mCurrentSubtype.getExtraValueOf(key) : null;
+    }
+
+    public String getCurrentSubtypeMode() {
+        return null != mCurrentSubtype ? mCurrentSubtype.getMode() : KEYBOARD_MODE;
+    }
+
+
     // A list of locales which are supported by default for voice input, unless we get a
     // different list from Gservices.
     private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES =
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index f774ce3..fe7aac7 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -24,23 +24,20 @@
 import java.util.List;
 
 public class SuggestedWords {
-    public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, false, null);
+    public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, null);
 
     public final List<CharSequence> mWords;
-    public final boolean mIsApplicationSpecifiedCompletions;
     public final boolean mTypedWordValid;
     public final boolean mHasMinimalSuggestion;
     public final List<SuggestedWordInfo> mSuggestedWordInfoList;
 
-    private SuggestedWords(List<CharSequence> words, boolean isApplicationSpecifiedCompletions,
-            boolean typedWordValid, boolean hasMinamlSuggestion,
-            List<SuggestedWordInfo> suggestedWordInfoList) {
+    private SuggestedWords(List<CharSequence> words, boolean typedWordValid,
+            boolean hasMinamlSuggestion, List<SuggestedWordInfo> suggestedWordInfoList) {
         if (words != null) {
             mWords = words;
         } else {
             mWords = Collections.emptyList();
         }
-        mIsApplicationSpecifiedCompletions = isApplicationSpecifiedCompletions;
         mTypedWordValid = typedWordValid;
         mHasMinimalSuggestion = hasMinamlSuggestion;
         mSuggestedWordInfoList = suggestedWordInfoList;
@@ -64,7 +61,6 @@
 
     public static class Builder {
         private List<CharSequence> mWords = new ArrayList<CharSequence>();
-        private boolean mIsCompletions;
         private boolean mTypedWordValid;
         private boolean mHasMinimalSuggestion;
         private List<SuggestedWordInfo> mSuggestedWordInfoList =
@@ -109,7 +105,6 @@
         public Builder setApplicationSpecifiedCompletions(CompletionInfo[] infos) {
             for (CompletionInfo info : infos)
                 addWord(info.getText());
-            mIsCompletions = true;
             return this;
         }
 
@@ -141,15 +136,14 @@
                     alreadySeen.add(prevWord);
                 }
             }
-            mIsCompletions = false;
             mTypedWordValid = false;
             mHasMinimalSuggestion = false;
             return this;
         }
 
         public SuggestedWords build() {
-            return new SuggestedWords(mWords, mIsCompletions, mTypedWordValid,
-                    mHasMinimalSuggestion, mSuggestedWordInfoList);
+            return new SuggestedWords(mWords, mTypedWordValid, mHasMinimalSuggestion,
+                    mSuggestedWordInfoList);
         }
 
         public int size() {
diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java
index c8b8c33..a4e0b35 100644
--- a/java/src/com/android/inputmethod/latin/TextEntryState.java
+++ b/java/src/com/android/inputmethod/latin/TextEntryState.java
@@ -16,117 +16,39 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.keyboard.Key;
-
-import android.content.Context;
-import android.text.format.DateFormat;
 import android.util.Log;
 
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Calendar;
-
 public class TextEntryState {
-    
-    private static final boolean DBG = false;
+    private static final String TAG = TextEntryState.class.getSimpleName();
+    private static final boolean DEBUG = true;
 
-    private static final String TAG = "TextEntryState";
+    private static final int UNKNOWN = 0;
+    private static final int START = 1;
+    private static final int IN_WORD = 2;
+    private static final int ACCEPTED_DEFAULT = 3;
+    private static final int PICKED_SUGGESTION = 4;
+    private static final int PUNCTUATION_AFTER_WORD = 5;
+    private static final int PUNCTUATION_AFTER_ACCEPTED = 6;
+    private static final int SPACE_AFTER_ACCEPTED = 7;
+    private static final int SPACE_AFTER_PICKED = 8;
+    private static final int UNDO_COMMIT = 9;
+    private static final int RECORRECTING = 10;
+    private static final int PICKED_RECORRECTION = 11;
 
-    private static boolean LOGGING = false;
+    private static int sState = UNKNOWN;
+    private static int sPreviousState = UNKNOWN;
 
-    private static int sBackspaceCount = 0;
-    
-    private static int sAutoSuggestCount = 0;
-    
-    private static int sAutoSuggestUndoneCount = 0;
-    
-    private static int sManualSuggestCount = 0;
-    
-    private static int sWordNotInDictionaryCount = 0;
-    
-    private static int sSessionCount = 0;
-    
-    private static int sTypedChars;
-
-    private static int sActualChars;
-
-    public enum State {
-        UNKNOWN,
-        START,
-        IN_WORD,
-        ACCEPTED_DEFAULT,
-        PICKED_SUGGESTION,
-        PUNCTUATION_AFTER_WORD,
-        PUNCTUATION_AFTER_ACCEPTED,
-        SPACE_AFTER_ACCEPTED,
-        SPACE_AFTER_PICKED,
-        UNDO_COMMIT,
-        RECORRECTING,
-        PICKED_RECORRECTION,
+    private static void setState(final int newState) {
+        sPreviousState = sState;
+        sState = newState;
     }
 
-    private static State sState = State.UNKNOWN;
-
-    private static FileOutputStream sKeyLocationFile;
-    private static FileOutputStream sUserActionFile;
-    
-    public static void newSession(Context context) {
-        sSessionCount++;
-        sAutoSuggestCount = 0;
-        sBackspaceCount = 0;
-        sAutoSuggestUndoneCount = 0;
-        sManualSuggestCount = 0;
-        sWordNotInDictionaryCount = 0;
-        sTypedChars = 0;
-        sActualChars = 0;
-        sState = State.START;
-        
-        if (LOGGING) {
-            try {
-                sKeyLocationFile = context.openFileOutput("key.txt", Context.MODE_APPEND);
-                sUserActionFile = context.openFileOutput("action.txt", Context.MODE_APPEND);
-            } catch (IOException ioe) {
-                Log.e("TextEntryState", "Couldn't open file for output: " + ioe);
-            }
-        }
-    }
-    
-    public static void endSession() {
-        if (sKeyLocationFile == null) {
-            return;
-        }
-        try {
-            sKeyLocationFile.close();
-            // Write to log file
-            // Write timestamp, settings,
-            String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime())
-                    .toString()
-                    + " BS: " + sBackspaceCount
-                    + " auto: " + sAutoSuggestCount
-                    + " manual: " + sManualSuggestCount
-                    + " typed: " + sWordNotInDictionaryCount
-                    + " undone: " + sAutoSuggestUndoneCount
-                    + " saved: " + ((float) (sActualChars - sTypedChars) / sActualChars)
-                    + "\n";
-            sUserActionFile.write(out.getBytes());
-            sUserActionFile.close();
-            sKeyLocationFile = null;
-            sUserActionFile = null;
-        } catch (IOException ioe) {
-            // ignore
-        }
-    }
-    
     public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) {
         if (typedWord == null) return;
-        if (!typedWord.equals(actualWord)) {
-            sAutoSuggestCount++;
-        }
-        sTypedChars += typedWord.length();
-        sActualChars += actualWord.length();
-        sState = State.ACCEPTED_DEFAULT;
+        setState(ACCEPTED_DEFAULT);
         LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString());
-        displayState();
+        if (DEBUG)
+            displayState("acceptedDefault", "typedWord", typedWord, "actualWord", actualWord);
     }
 
     // State.ACCEPTED_DEFAULT will be changed to other sub-states
@@ -138,151 +60,167 @@
         case SPACE_AFTER_ACCEPTED:
         case PUNCTUATION_AFTER_ACCEPTED:
         case IN_WORD:
-            sState = State.ACCEPTED_DEFAULT;
+            setState(ACCEPTED_DEFAULT);
             break;
         default:
             break;
         }
-        displayState();
+        if (DEBUG) displayState("backToAcceptedDefault", "typedWord", typedWord);
     }
 
-    public static void acceptedTyped(@SuppressWarnings("unused") CharSequence typedWord) {
-        sWordNotInDictionaryCount++;
-        sState = State.PICKED_SUGGESTION;
-        displayState();
+    public static void acceptedTyped(CharSequence typedWord) {
+        setState(PICKED_SUGGESTION);
+        if (DEBUG) displayState("acceptedTyped", "typedWord", typedWord);
     }
 
     public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) {
-        sManualSuggestCount++;
-        State oldState = sState;
-        if (typedWord.equals(actualWord)) {
-            acceptedTyped(typedWord);
-        }
-        if (oldState == State.RECORRECTING || oldState == State.PICKED_RECORRECTION) {
-            sState = State.PICKED_RECORRECTION;
+        if (sState == RECORRECTING || sState == PICKED_RECORRECTION) {
+            setState(PICKED_RECORRECTION);
         } else {
-            sState = State.PICKED_SUGGESTION;
+            setState(PICKED_SUGGESTION);
         }
-        displayState();
+        if (DEBUG)
+            displayState("acceptedSuggestion", "typedWord", typedWord, "actualWord", actualWord);
     }
 
     public static void selectedForRecorrection() {
-        sState = State.RECORRECTING;
-        displayState();
+        setState(RECORRECTING);
+        if (DEBUG) displayState("selectedForRecorrection");
     }
 
     public static void onAbortRecorrection() {
-        if (isRecorrecting()) {
-            sState = State.START;
+        if (sState == RECORRECTING || sState == PICKED_RECORRECTION) {
+            setState(START);
         }
-        displayState();
+        if (DEBUG) displayState("onAbortRecorrection");
     }
 
     public static void typedCharacter(char c, boolean isSeparator) {
-        boolean isSpace = c == ' ';
+        final boolean isSpace = (c == ' ');
         switch (sState) {
-            case IN_WORD:
-                if (isSpace || isSeparator) {
-                    sState = State.START;
-                } else {
-                    // State hasn't changed.
-                }
-                break;
-            case ACCEPTED_DEFAULT:
-            case SPACE_AFTER_PICKED:
-                if (isSpace) {
-                    sState = State.SPACE_AFTER_ACCEPTED;
-                } else if (isSeparator) {
-                    sState = State.PUNCTUATION_AFTER_ACCEPTED;
-                } else {
-                    sState = State.IN_WORD;
-                }
-                break;
-            case PICKED_SUGGESTION:
-            case PICKED_RECORRECTION:
-                if (isSpace) {
-                    sState = State.SPACE_AFTER_PICKED;
-                } else if (isSeparator) {
-                    // Swap 
-                    sState = State.PUNCTUATION_AFTER_ACCEPTED;
-                } else {
-                    sState = State.IN_WORD;
-                }
-                break;
-            case START:
-            case UNKNOWN:
-            case SPACE_AFTER_ACCEPTED:
-            case PUNCTUATION_AFTER_ACCEPTED:
-            case PUNCTUATION_AFTER_WORD:
-                if (!isSpace && !isSeparator) {
-                    sState = State.IN_WORD;
-                } else {
-                    sState = State.START;
-                }
-                break;
-            case UNDO_COMMIT:
-                if (isSpace || isSeparator) {
-                    sState = State.ACCEPTED_DEFAULT;
-                } else {
-                    sState = State.IN_WORD;
-                }
-                break;
-            case RECORRECTING:
-                sState = State.START;
-                break;
+        case IN_WORD:
+            if (isSpace || isSeparator) {
+                setState(START);
+            } else {
+                // State hasn't changed.
+            }
+            break;
+        case ACCEPTED_DEFAULT:
+        case SPACE_AFTER_PICKED:
+        case PUNCTUATION_AFTER_ACCEPTED:
+            if (isSpace) {
+                setState(SPACE_AFTER_ACCEPTED);
+            } else if (isSeparator) {
+                // Swap
+                setState(PUNCTUATION_AFTER_ACCEPTED);
+            } else {
+                setState(IN_WORD);
+            }
+            break;
+        case PICKED_SUGGESTION:
+        case PICKED_RECORRECTION:
+            if (isSpace) {
+                setState(SPACE_AFTER_PICKED);
+            } else if (isSeparator) {
+                // Swap
+                setState(PUNCTUATION_AFTER_ACCEPTED);
+            } else {
+                setState(IN_WORD);
+            }
+            break;
+        case START:
+        case UNKNOWN:
+        case SPACE_AFTER_ACCEPTED:
+        case PUNCTUATION_AFTER_WORD:
+            if (!isSpace && !isSeparator) {
+                setState(IN_WORD);
+            } else {
+                setState(START);
+            }
+            break;
+        case UNDO_COMMIT:
+            if (isSpace || isSeparator) {
+                setState(ACCEPTED_DEFAULT);
+            } else {
+                setState(IN_WORD);
+            }
+            break;
+        case RECORRECTING:
+            setState(START);
+            break;
         }
-        displayState();
+        if (DEBUG) displayState("typedCharacter", "char", c, "isSeparator", isSeparator);
     }
     
     public static void backspace() {
-        if (sState == State.ACCEPTED_DEFAULT) {
-            sState = State.UNDO_COMMIT;
-            sAutoSuggestUndoneCount++;
+        if (sState == ACCEPTED_DEFAULT) {
+            setState(UNDO_COMMIT);
             LatinImeLogger.logOnAutoSuggestionCanceled();
-        } else if (sState == State.UNDO_COMMIT) {
-            sState = State.IN_WORD;
+        } else if (sState == UNDO_COMMIT) {
+            setState(IN_WORD);
         }
-        sBackspaceCount++;
-        displayState();
+        if (DEBUG) displayState("backspace");
     }
 
     public static void reset() {
-        sState = State.START;
-        displayState();
+        setState(START);
+        if (DEBUG) displayState("reset");
     }
 
-    public static State getState() {
-        if (DBG) {
-            Log.d(TAG, "Returning state = " + sState);
-        }
-        return sState;
+    public static boolean isAcceptedDefault() {
+        return sState == ACCEPTED_DEFAULT;
+    }
+
+    public static boolean isSpaceAfterPicked() {
+        return sState == SPACE_AFTER_PICKED;
+    }
+
+    public static boolean isUndoCommit() {
+        return sState == UNDO_COMMIT;
+    }
+
+    public static boolean isPunctuationAfterAccepted() {
+        return sState == PUNCTUATION_AFTER_ACCEPTED;
     }
 
     public static boolean isRecorrecting() {
-        return sState == State.RECORRECTING || sState == State.PICKED_RECORRECTION;
+        return sState == RECORRECTING || sState == PICKED_RECORRECTION;
     }
 
-    public static void keyPressedAt(Key key, int x, int y) {
-        if (LOGGING && sKeyLocationFile != null && key.mCode >= 32) {
-            String out =
-                    "KEY: " + (char) key.mCode
-                    + " X: " + x
-                    + " Y: " + y
-                    + " MX: " + (key.mX + key.mWidth / 2)
-                    + " MY: " + (key.mY + key.mHeight / 2)
-                    + "\n";
-            try {
-                sKeyLocationFile.write(out.getBytes());
-            } catch (IOException ioe) {
-                // TODO: May run out of space
-            }
+    public static String getState() {
+        return stateName(sState);
+    }
+
+    private static String stateName(int state) {
+        switch (state) {
+        case START: return "START";
+        case IN_WORD: return "IN_WORD";
+        case ACCEPTED_DEFAULT: return "ACCEPTED_DEFAULT";
+        case PICKED_SUGGESTION: return "PICKED_SUGGESTION";
+        case PUNCTUATION_AFTER_WORD: return "PUNCTUATION_AFTER_WORD";
+        case PUNCTUATION_AFTER_ACCEPTED: return "PUNCTUATION_AFTER_ACCEPTED";
+        case SPACE_AFTER_ACCEPTED: return "SPACE_AFTER_ACCEPTED";
+        case SPACE_AFTER_PICKED: return "SPACE_AFTER_PICKED";
+        case UNDO_COMMIT: return "UNDO_COMMIT";
+        case RECORRECTING: return "RECORRECTING";
+        case PICKED_RECORRECTION: return "PICKED_RECORRECTION";
+        default: return "UNKNOWN";
         }
     }
 
-    private static void displayState() {
-        if (DBG) {
-            Log.d(TAG, "State = " + sState);
+    private static void displayState(String title, Object ... args) {
+        final StringBuilder sb = new StringBuilder(title);
+        sb.append(':');
+        for (int i = 0; i < args.length; i += 2) {
+            sb.append(' ');
+            sb.append(args[i]);
+            sb.append('=');
+            sb.append(args[i+1].toString());
         }
+        sb.append(" state=");
+        sb.append(stateName(sState));
+        sb.append(" previous=");
+        sb.append(stateName(sPreviousState));
+        Log.d(TAG, sb.toString());
     }
 }
-
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index b10dd6d..555a522 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -126,7 +126,8 @@
 
 static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jobject object, jint dict,
         jint proximityInfo, jintArray xCoordinatesArray, jintArray yCoordinatesArray,
-        jintArray inputArray, jint arraySize, jcharArray outputArray, jintArray frequencyArray) {
+        jintArray inputArray, jint arraySize, jint flags,
+        jcharArray outputArray, jintArray frequencyArray) {
     Dictionary *dictionary = (Dictionary*)dict;
     if (!dictionary) return 0;
     ProximityInfo *pInfo = (ProximityInfo*)proximityInfo;
@@ -140,7 +141,7 @@
     jchar *outputChars = env->GetCharArrayElements(outputArray, NULL);
 
     int count = dictionary->getSuggestions(pInfo, xCoordinates, yCoordinates, inputCodes,
-            arraySize, (unsigned short*) outputChars, frequencies);
+            arraySize, flags, (unsigned short*) outputChars, frequencies);
 
     env->ReleaseIntArrayElements(frequencyArray, frequencies, 0);
     env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT);
@@ -213,7 +214,7 @@
 static JNINativeMethod sMethods[] = {
     {"openNative", "(Ljava/lang/String;JJIIIII)I", (void*)latinime_BinaryDictionary_open},
     {"closeNative", "(I)V", (void*)latinime_BinaryDictionary_close},
-    {"getSuggestionsNative", "(II[I[I[II[C[I)I", (void*)latinime_BinaryDictionary_getSuggestions},
+    {"getSuggestionsNative", "(II[I[I[III[C[I)I", (void*)latinime_BinaryDictionary_getSuggestions},
     {"isValidWordNative", "(I[CI)Z", (void*)latinime_BinaryDictionary_isValidWord},
     {"getBigramsNative", "(I[CI[II[C[IIII)I", (void*)latinime_BinaryDictionary_getBigrams}
 };
diff --git a/native/src/debug.h b/native/src/debug.h
index e5572e1..ae629b2 100644
--- a/native/src/debug.h
+++ b/native/src/debug.h
@@ -55,4 +55,15 @@
     // usleep(10);
 }
 
+static inline void printDebug(const char* tag, int* codes, int codesSize, int MAX_PROXIMITY_CHARS) {
+    unsigned char *buf = (unsigned char*)malloc((1 + codesSize) * sizeof(*buf));
+
+    buf[codesSize] = 0;
+    while (--codesSize >= 0)
+        buf[codesSize] = (unsigned char)codes[codesSize * MAX_PROXIMITY_CHARS];
+    LOGI("%s, WORD = %s", tag, buf);
+
+    free(buf);
+}
+
 #endif // LATINIME_DEBUG_H
diff --git a/native/src/dictionary.h b/native/src/dictionary.h
index fbbb831..13b2a28 100644
--- a/native/src/dictionary.h
+++ b/native/src/dictionary.h
@@ -29,9 +29,9 @@
     Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust, int typedLetterMultipler,
             int fullWordMultiplier, int maxWordLength, int maxWords, int maxAlternatives);
     int getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates, int *ycoordinates,
-            int *codes, int codesSize, unsigned short *outWords, int *frequencies) {
+            int *codes, int codesSize, int flags, unsigned short *outWords, int *frequencies) {
         return mUnigramDictionary->getSuggestions(proximityInfo, xcoordinates, ycoordinates, codes,
-                codesSize, outWords, frequencies);
+                codesSize, flags, outWords, frequencies);
     }
 
     // TODO: Call mBigramDictionary instead of mUnigramDictionary
diff --git a/native/src/unigram_dictionary.cpp b/native/src/unigram_dictionary.cpp
index 72b0f36..9aa36b0 100644
--- a/native/src/unigram_dictionary.cpp
+++ b/native/src/unigram_dictionary.cpp
@@ -29,20 +29,136 @@
 
 namespace latinime {
 
+const UnigramDictionary::digraph_t UnigramDictionary::GERMAN_UMLAUT_DIGRAPHS[] =
+        { { 'a', 'e' },
+        { 'o', 'e' },
+        { 'u', 'e' } };
+
 UnigramDictionary::UnigramDictionary(const unsigned char *dict, int typedLetterMultiplier,
         int fullWordMultiplier, int maxWordLength, int maxWords, int maxProximityChars,
         const bool isLatestDictVersion)
     : DICT(dict), MAX_WORD_LENGTH(maxWordLength), MAX_WORDS(maxWords),
     MAX_PROXIMITY_CHARS(maxProximityChars), IS_LATEST_DICT_VERSION(isLatestDictVersion),
     TYPED_LETTER_MULTIPLIER(typedLetterMultiplier), FULL_WORD_MULTIPLIER(fullWordMultiplier),
-    ROOT_POS(isLatestDictVersion ? DICTIONARY_HEADER_SIZE : 0) {
+    ROOT_POS(isLatestDictVersion ? DICTIONARY_HEADER_SIZE : 0),
+    BYTES_IN_ONE_CHAR(MAX_PROXIMITY_CHARS * sizeof(*mInputCodes)) {
     if (DEBUG_DICT) LOGI("UnigramDictionary - constructor");
 }
 
 UnigramDictionary::~UnigramDictionary() {}
 
-int UnigramDictionary::getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates,
-        int *ycoordinates, int *codes, int codesSize, unsigned short *outWords, int *frequencies) {
+static inline unsigned int getCodesBufferSize(const int* codes, const int codesSize,
+        const int MAX_PROXIMITY_CHARS) {
+    return sizeof(*codes) * MAX_PROXIMITY_CHARS * codesSize;
+}
+
+bool UnigramDictionary::isDigraph(const int* codes, const int i, const int codesSize) const {
+
+    // There can't be a digraph if we don't have at least 2 characters to examine
+    if (i + 2 > codesSize) return false;
+
+    // Search for the first char of some digraph
+    int lastDigraphIndex = -1;
+    const int thisChar = codes[i * MAX_PROXIMITY_CHARS];
+    for (lastDigraphIndex = sizeof(GERMAN_UMLAUT_DIGRAPHS) / sizeof(GERMAN_UMLAUT_DIGRAPHS[0]) - 1;
+            lastDigraphIndex >= 0; --lastDigraphIndex) {
+        if (thisChar == GERMAN_UMLAUT_DIGRAPHS[lastDigraphIndex].first) break;
+    }
+    // No match: return early
+    if (lastDigraphIndex < 0) return false;
+
+    // It's an interesting digraph if the second char matches too.
+    return GERMAN_UMLAUT_DIGRAPHS[lastDigraphIndex].second == codes[(i + 1) * MAX_PROXIMITY_CHARS];
+}
+
+// Mostly the same arguments as the non-recursive version, except:
+// codes is the original value. It points to the start of the work buffer, and gets passed as is.
+// codesSize is the size of the user input (thus, it is the size of codesSrc).
+// codesDest is the current point in the work buffer.
+// codesSrc is the current point in the user-input, original, content-unmodified buffer.
+// codesRemain is the remaining size in codesSrc.
+void UnigramDictionary::getWordWithDigraphSuggestionsRec(const ProximityInfo *proximityInfo,
+        const int *xcoordinates, const int* ycoordinates, const int *codesBuffer,
+        const int codesBufferSize, const int flags, const int* codesSrc, const int codesRemain,
+        int* codesDest, unsigned short* outWords, int* frequencies) {
+
+    for (int i = 0; i < codesRemain; ++i) {
+        if (isDigraph(codesSrc, i, codesRemain)) {
+            // Found a digraph. We will try both spellings. eg. the word is "pruefen"
+
+            // Copy the word up to the first char of the digraph, then continue processing
+            // on the remaining part of the word, skipping the second char of the digraph.
+            // In our example, copy "pru" and continue running on "fen"
+            memcpy(codesDest, codesSrc, i * BYTES_IN_ONE_CHAR);
+            getWordWithDigraphSuggestionsRec(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
+                    codesBufferSize, flags, codesSrc + (i + 1) * MAX_PROXIMITY_CHARS,
+                    codesRemain - i - 1, codesDest + i * MAX_PROXIMITY_CHARS,
+                    outWords, frequencies);
+
+            // Copy the second char of the digraph in place, then continue processing on
+            // the remaining part of the word.
+            // In our example, after "pru" in the buffer copy the "e", and continue running on "fen"
+            memcpy(codesDest + i * MAX_PROXIMITY_CHARS, codesSrc + i * MAX_PROXIMITY_CHARS,
+                    BYTES_IN_ONE_CHAR);
+            getWordWithDigraphSuggestionsRec(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
+                    codesBufferSize, flags, codesSrc + i * MAX_PROXIMITY_CHARS, codesRemain - i,
+                    codesDest + i * MAX_PROXIMITY_CHARS, outWords, frequencies);
+            return;
+        }
+    }
+
+    // If we come here, we hit the end of the word: let's check it against the dictionary.
+    // In our example, we'll come here once for "prufen" and then once for "pruefen".
+    // If the word contains several digraphs, we'll come it for the product of them.
+    // eg. if the word is "ueberpruefen" we'll test, in order, against
+    // "uberprufen", "uberpruefen", "ueberprufen", "ueberpruefen".
+    const unsigned int remainingBytes = BYTES_IN_ONE_CHAR * codesRemain;
+    if (0 != remainingBytes)
+        memcpy(codesDest, codesSrc, remainingBytes);
+
+    getWordSuggestions(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
+            (codesDest - codesBuffer) / MAX_PROXIMITY_CHARS + codesRemain, outWords, frequencies);
+}
+
+int UnigramDictionary::getSuggestions(const ProximityInfo *proximityInfo, const int *xcoordinates,
+        const int *ycoordinates, const int *codes, const int codesSize, const int flags,
+        unsigned short *outWords, int *frequencies) {
+
+    if (REQUIRES_GERMAN_UMLAUT_PROCESSING & flags)
+    { // Incrementally tune the word and try all possibilities
+        int codesBuffer[getCodesBufferSize(codes, codesSize, MAX_PROXIMITY_CHARS)];
+        getWordWithDigraphSuggestionsRec(proximityInfo, xcoordinates, ycoordinates, codesBuffer,
+                codesSize, flags, codes, codesSize, codesBuffer, outWords, frequencies);
+    } else { // Normal processing
+        getWordSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, codesSize,
+                outWords, frequencies);
+    }
+
+    PROF_START(6);
+    // Get the word count
+    int suggestedWordsCount = 0;
+    while (suggestedWordsCount < MAX_WORDS && mFrequencies[suggestedWordsCount] > 0) {
+        suggestedWordsCount++;
+    }
+
+    if (DEBUG_DICT) {
+        LOGI("Returning %d words", suggestedWordsCount);
+        LOGI("Next letters: ");
+        for (int k = 0; k < NEXT_LETTERS_SIZE; k++) {
+            if (mNextLettersFrequency[k] > 0) {
+                LOGI("%c = %d,", k, mNextLettersFrequency[k]);
+            }
+        }
+    }
+    PROF_END(6);
+    PROF_CLOSE;
+    return suggestedWordsCount;
+}
+
+void UnigramDictionary::getWordSuggestions(const ProximityInfo *proximityInfo,
+        const int *xcoordinates, const int *ycoordinates, const int *codes, const int codesSize,
+        unsigned short *outWords, int *frequencies) {
+
     PROF_OPEN;
     PROF_START(0);
     initSuggestions(codes, codesSize, outWords, frequencies);
@@ -103,30 +219,10 @@
         }
     }
     PROF_END(5);
-
-    PROF_START(6);
-    // Get the word count
-    int suggestedWordsCount = 0;
-    while (suggestedWordsCount < MAX_WORDS && mFrequencies[suggestedWordsCount] > 0) {
-        suggestedWordsCount++;
-    }
-
-    if (DEBUG_DICT) {
-        LOGI("Returning %d words", suggestedWordsCount);
-        LOGI("Next letters: ");
-        for (int k = 0; k < NEXT_LETTERS_SIZE; k++) {
-            if (mNextLettersFrequency[k] > 0) {
-                LOGI("%c = %d,", k, mNextLettersFrequency[k]);
-            }
-        }
-    }
-    PROF_END(6);
-    PROF_CLOSE;
-    return suggestedWordsCount;
 }
 
-void UnigramDictionary::initSuggestions(int *codes, int codesSize, unsigned short *outWords,
-        int *frequencies) {
+void UnigramDictionary::initSuggestions(const int *codes, const int codesSize,
+        unsigned short *outWords, int *frequencies) {
     if (DEBUG_DICT) LOGI("initSuggest");
     mFrequencies = frequencies;
     mOutputChars = outWords;
@@ -204,7 +300,7 @@
     if (length != mInputLength) {
         return false;
     }
-    int *inputCodes = mInputCodes;
+    const int *inputCodes = mInputCodes;
     while (length--) {
         if ((unsigned int) *inputCodes != (unsigned int) *word) {
             return false;
@@ -423,7 +519,7 @@
     const int currentChar = *getInputCharsAt(inputIndex);
     const int leftIndex = inputIndex - 1;
     if (leftIndex >= 0) {
-        int *leftChars = getInputCharsAt(leftIndex);
+        const int *leftChars = getInputCharsAt(leftIndex);
         int i = 0;
         while (leftChars[i] > 0 && i < MAX_PROXIMITY_CHARS) {
             if (leftChars[i++] == currentChar) return true;
@@ -431,7 +527,7 @@
     }
     const int rightIndex = inputIndex + 1;
     if (rightIndex < inputLength) {
-        int *rightChars = getInputCharsAt(rightIndex);
+        const int *rightChars = getInputCharsAt(rightIndex);
         int i = 0;
         while (rightChars[i] > 0 && i < MAX_PROXIMITY_CHARS) {
             if (rightChars[i++] == currentChar) return true;
@@ -523,7 +619,7 @@
         *newDiffs = diffs;
         *newInputIndex = inputIndex;
     } else {
-        int *currentChars = getInputCharsAt(inputIndex);
+        const int *currentChars = getInputCharsAt(inputIndex);
 
         if (transposedPos >= 0) {
             if (inputIndex == transposedPos) currentChars += MAX_PROXIMITY_CHARS;
diff --git a/native/src/unigram_dictionary.h b/native/src/unigram_dictionary.h
index e84875b..a959845 100644
--- a/native/src/unigram_dictionary.h
+++ b/native/src/unigram_dictionary.h
@@ -33,12 +33,22 @@
 public:
     UnigramDictionary(const unsigned char *dict, int typedLetterMultipler, int fullWordMultiplier,
             int maxWordLength, int maxWords, int maxProximityChars, const bool isLatestDictVersion);
-    int getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates, int *ycoordinates,
-            int *codes, int codesSize, unsigned short *outWords, int *frequencies);
+    int getSuggestions(const ProximityInfo *proximityInfo, const int *xcoordinates,
+            const int *ycoordinates, const int *codes, const int codesSize, const int flags,
+            unsigned short *outWords, int *frequencies);
     ~UnigramDictionary();
 
 private:
-    void initSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies);
+    void getWordSuggestions(const ProximityInfo *proximityInfo, const int *xcoordinates,
+            const int *ycoordinates, const int *codes, const int codesSize,
+            unsigned short *outWords, int *frequencies);
+    bool isDigraph(const int* codes, const int i, const int codesSize) const;
+    void getWordWithDigraphSuggestionsRec(const ProximityInfo *proximityInfo,
+        const int *xcoordinates, const int* ycoordinates, const int *codesBuffer,
+        const int codesBufferSize, const int flags, const int* codesSrc, const int codesRemain,
+        int* codesDest, unsigned short* outWords, int* frequencies);
+    void initSuggestions(const int *codes, const int codesSize, unsigned short *outWords,
+            int *frequencies);
     void getSuggestionCandidates(const int skipPos, const int excessivePos,
             const int transposedPos, int *nextLetters, const int nextLettersSize,
             const int maxDepth);
@@ -86,7 +96,7 @@
             const int startInputIndex, const int depth, unsigned short *word,
             int *newChildPosition, int *newCount, bool *newTerminal, int *newFreq, int *siblingPos);
     bool existsAdjacentProximityChars(const int inputIndex, const int inputLength);
-    inline int* getInputCharsAt(const int index) {
+    inline const int* getInputCharsAt(const int index) {
         return mInputCodes + (index * MAX_PROXIMITY_CHARS);
     }
     const unsigned char *DICT;
@@ -97,10 +107,20 @@
     const int TYPED_LETTER_MULTIPLIER;
     const int FULL_WORD_MULTIPLIER;
     const int ROOT_POS;
+    const unsigned int BYTES_IN_ONE_CHAR;
+
+    // Flags for special processing
+    // Those *must* match the flags in BinaryDictionary.Flags.ALL_FLAGS in BinaryDictionary.java
+    // or something very bad (like, the apocalypse) will happen.
+    // Please update both at the same time.
+    enum {
+        REQUIRES_GERMAN_UMLAUT_PROCESSING = 0x1
+    };
+    static const struct digraph_t { int first; int second; } GERMAN_UMLAUT_DIGRAPHS[];
 
     int *mFrequencies;
     unsigned short *mOutputChars;
-    int *mInputCodes;
+    const int *mInputCodes;
     int mInputLength;
     // MAX_WORD_LENGTH_INTERNAL must be bigger than MAX_WORD_LENGTH
     unsigned short mWord[MAX_WORD_LENGTH_INTERNAL];