diff --git a/dictionary/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/dictionary/jni/com_android_inputmethod_latin_BinaryDictionary.cpp index 3076085e4..3dda062fe 100644 --- a/dictionary/jni/com_android_inputmethod_latin_BinaryDictionary.cpp +++ b/dictionary/jni/com_android_inputmethod_latin_BinaryDictionary.cpp @@ -89,7 +89,7 @@ static jint latinime_BinaryDictionary_open static int latinime_BinaryDictionary_getSuggestions( JNIEnv *env, jobject object, jint dict, jintArray inputArray, jint arraySize, jcharArray outputArray, jintArray frequencyArray, jint maxWordLength, jint maxWords, - jint maxAlternatives, jint skipPos) + jint maxAlternatives, jint skipPos, jintArray nextLettersArray, jint nextLettersSize) { Dictionary *dictionary = (Dictionary*) dict; if (dictionary == NULL) @@ -98,13 +98,16 @@ static int latinime_BinaryDictionary_getSuggestions( int *frequencies = env->GetIntArrayElements(frequencyArray, NULL); int *inputCodes = env->GetIntArrayElements(inputArray, NULL); jchar *outputChars = env->GetCharArrayElements(outputArray, NULL); + int *nextLetters = nextLettersArray != NULL ? env->GetIntArrayElements(nextLettersArray, NULL) + : NULL; int count = dictionary->getSuggestions(inputCodes, arraySize, (unsigned short*) outputChars, frequencies, - maxWordLength, maxWords, maxAlternatives, skipPos); + maxWordLength, maxWords, maxAlternatives, skipPos, nextLetters, nextLettersSize); env->ReleaseIntArrayElements(frequencyArray, frequencies, 0); env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT); env->ReleaseCharArrayElements(outputArray, outputChars, 0); + env->ReleaseIntArrayElements(nextLettersArray, nextLetters, 0); return count; } @@ -136,7 +139,7 @@ static JNINativeMethod gMethods[] = { {"openNative", "(Landroid/content/res/AssetManager;Ljava/lang/String;II)I", (void*)latinime_BinaryDictionary_open}, {"closeNative", "(I)V", (void*)latinime_BinaryDictionary_close}, - {"getSuggestionsNative", "(I[II[C[IIIII)I", (void*)latinime_BinaryDictionary_getSuggestions}, + {"getSuggestionsNative", "(I[II[C[IIIII[II)I", (void*)latinime_BinaryDictionary_getSuggestions}, {"isValidWordNative", "(I[CI)Z", (void*)latinime_BinaryDictionary_isValidWord} }; diff --git a/dictionary/src/dictionary.cpp b/dictionary/src/dictionary.cpp index 306aff527..6e6f44182 100644 --- a/dictionary/src/dictionary.cpp +++ b/dictionary/src/dictionary.cpp @@ -49,7 +49,8 @@ Dictionary::~Dictionary() } int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies, - int maxWordLength, int maxWords, int maxAlternatives, int skipPos) + int maxWordLength, int maxWords, int maxAlternatives, int skipPos, + int *nextLetters, int nextLettersSize) { int suggWords; mFrequencies = frequencies; @@ -61,6 +62,8 @@ int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWor mMaxWords = maxWords; mSkipPos = skipPos; mMaxEditDistance = mInputLength < 5 ? 2 : mInputLength / 2; + mNextLettersFrequencies = nextLetters; + mNextLettersSize = nextLettersSize; getWordsRec(0, 0, mInputLength * 3, false, 1, 0, 0); @@ -68,9 +71,27 @@ int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWor suggWords = 0; while (suggWords < mMaxWords && mFrequencies[suggWords] > 0) suggWords++; if (DEBUG_DICT) LOGI("Returning %d words", suggWords); + + if (DEBUG_DICT) { + LOGI("Next letters: "); + for (int k = 0; k < nextLettersSize; k++) { + if (mNextLettersFrequencies[k] > 0) { + LOGI("%c = %d,", k, mNextLettersFrequencies[k]); + } + } + LOGI("\n"); + } return suggWords; } +void +Dictionary::registerNextLetter(unsigned short c) +{ + if (c < mNextLettersSize) { + mNextLettersFrequencies[c]++; + } +} + unsigned short Dictionary::getChar(int *pos) { @@ -210,6 +231,9 @@ Dictionary::getWordsRec(int pos, int depth, int maxDepth, bool completion, int s mWord[depth] = c; if (terminal) { addWord(mWord, depth + 1, freq * snr); + if (depth >= mInputLength && mSkipPos < 0) { + registerNextLetter(mWord[mInputLength]); + } } if (childrenAddress != 0) { getWordsRec(childrenAddress, depth + 1, maxDepth, diff --git a/dictionary/src/dictionary.h b/dictionary/src/dictionary.h index a12c035c8..3749f3d88 100644 --- a/dictionary/src/dictionary.h +++ b/dictionary/src/dictionary.h @@ -32,7 +32,8 @@ class Dictionary { public: Dictionary(void *dict, int typedLetterMultipler, int fullWordMultiplier); int getSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies, - int maxWordLength, int maxWords, int maxAlternatives, int skipPos); + int maxWordLength, int maxWords, int maxAlternatives, int skipPos, + int *nextLetters, int nextLettersSize); bool isValidWord(unsigned short *word, int length); void setAsset(void *asset) { mAsset = asset; } void *getAsset() { return mAsset; } @@ -53,6 +54,7 @@ private: void getWordsRec(int pos, int depth, int maxDepth, bool completion, int frequency, int inputIndex, int diffs); bool isValidWordRec(int pos, unsigned short *word, int offset, int length); + void registerNextLetter(unsigned short c); unsigned char *mDict; void *mAsset; @@ -70,6 +72,8 @@ private: int mFullWordMultiplier; int mTypedLetterMultiplier; + int *mNextLettersFrequencies; + int mNextLettersSize; }; // ---------------------------------------------------------------------------- diff --git a/src/com/android/inputmethod/latin/BinaryDictionary.java b/src/com/android/inputmethod/latin/BinaryDictionary.java index 68d8b740c..ec467c88d 100644 --- a/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -65,7 +65,8 @@ public class BinaryDictionary extends Dictionary { private native boolean isValidWordNative(int nativeData, char[] word, int wordLength); private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize, char[] outputChars, int[] frequencies, - int maxWordLength, int maxWords, int maxAlternatives, int skipPos); + int maxWordLength, int maxWords, int maxAlternatives, int skipPos, + int[] nextLettersFrequencies, int nextLettersSize); private final void loadDictionary(Context context, int resId) { AssetManager am = context.getResources().getAssets(); @@ -74,7 +75,8 @@ public class BinaryDictionary extends Dictionary { } @Override - public void getWords(final WordComposer codes, final WordCallback callback) { + public void getWords(final WordComposer codes, final WordCallback callback, + int[] nextLettersFrequencies) { final int codesSize = codes.size(); // Wont deal with really long words. if (codesSize > MAX_WORD_LENGTH - 1) return; @@ -90,7 +92,9 @@ public class BinaryDictionary extends Dictionary { int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, mOutputChars, mFrequencies, - MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1); + MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1, + nextLettersFrequencies, + nextLettersFrequencies != null ? nextLettersFrequencies.length : 0); // If there aren't sufficient suggestions, search for words by allowing wild cards at // the different character positions. This feature is not ready for prime-time as we need @@ -100,7 +104,8 @@ public class BinaryDictionary extends Dictionary { for (int skip = 0; skip < codesSize; skip++) { int tempCount = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, mOutputChars, mFrequencies, - MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip); + MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip, + null, 0); count = Math.max(count, tempCount); if (tempCount > 0) break; } diff --git a/src/com/android/inputmethod/latin/ContactsDictionary.java b/src/com/android/inputmethod/latin/ContactsDictionary.java index fd7ba55fc..f53ebf3f5 100644 --- a/src/com/android/inputmethod/latin/ContactsDictionary.java +++ b/src/com/android/inputmethod/latin/ContactsDictionary.java @@ -84,14 +84,15 @@ public class ContactsDictionary extends ExpandableDictionary { } @Override - public synchronized void getWords(final WordComposer codes, final WordCallback callback) { + public synchronized void getWords(final WordComposer codes, final WordCallback callback, + int[] nextLettersFrequencies) { synchronized (mUpdatingLock) { // If we need to update, start off a background task if (mRequiresReload) loadDictionaryAsyncLocked(); // Currently updating contacts, don't return any results. if (mUpdatingContacts) return; } - super.getWords(codes, callback); + super.getWords(codes, callback, nextLettersFrequencies); } @Override diff --git a/src/com/android/inputmethod/latin/Dictionary.java b/src/com/android/inputmethod/latin/Dictionary.java index 6c1c856e7..b656d04dc 100644 --- a/src/com/android/inputmethod/latin/Dictionary.java +++ b/src/com/android/inputmethod/latin/Dictionary.java @@ -55,9 +55,14 @@ abstract public class Dictionary { * words are added through the callback object. * @param composer the key sequence to match * @param callback the callback object to send matched words to as possible candidates + * @param nextLettersFrequencies array of frequencies of next letters that could follow the + * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have + * a non-zero value on returning from this method. + * Pass in null if you don't want the dictionary to look up next letters. * @see WordCallback#addWord(char[], int, int) */ - abstract public void getWords(final WordComposer composer, final WordCallback callback); + abstract public void getWords(final WordComposer composer, final WordCallback callback, + int[] nextLettersFrequencies); /** * Checks if the given word occurs in the dictionary diff --git a/src/com/android/inputmethod/latin/ExpandableDictionary.java b/src/com/android/inputmethod/latin/ExpandableDictionary.java index 1589168ee..648f577ca 100644 --- a/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -27,6 +27,7 @@ public class ExpandableDictionary extends Dictionary { private char[] mWordBuilder = new char[MAX_WORD_LENGTH]; private int mMaxDepth; private int mInputLength; + private int[] mNextLettersFrequencies; public static final int MAX_WORD_LENGTH = 32; private static final char QUOTE = '\''; @@ -116,8 +117,10 @@ public class ExpandableDictionary extends Dictionary { } @Override - public void getWords(final WordComposer codes, final WordCallback callback) { + public void getWords(final WordComposer codes, final WordCallback callback, + int[] nextLettersFrequencies) { mInputLength = codes.size(); + mNextLettersFrequencies = nextLettersFrequencies; if (mCodes.length < mInputLength) mCodes = new int[mInputLength][]; // Cache the codes so that we don't have to lookup an array list for (int i = 0; i < mInputLength; i++) { @@ -216,6 +219,11 @@ public class ExpandableDictionary extends Dictionary { if (!callback.addWord(word, 0, depth + 1, freq * snr)) { return; } + // Add to frequency of next letters for predictive correction + if (mNextLettersFrequencies != null && depth >= inputIndex && skipPos < 0 + && mNextLettersFrequencies.length > word[inputIndex]) { + mNextLettersFrequencies[word[inputIndex]]++; + } } if (children != null) { getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, diff --git a/src/com/android/inputmethod/latin/LatinIME.java b/src/com/android/inputmethod/latin/LatinIME.java index 56971a534..470b0048e 100644 --- a/src/com/android/inputmethod/latin/LatinIME.java +++ b/src/com/android/inputmethod/latin/LatinIME.java @@ -1340,6 +1340,8 @@ public class LatinIME extends InputMethodService private void updateSuggestions() { mSuggestionShouldReplaceCurrentWord = false; + ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); + // Check if we have a suggestion engine attached. if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) { return; @@ -1351,6 +1353,10 @@ public class LatinIME extends InputMethodService } List stringList = mSuggest.getSuggestions(mInputView, mWord, false); + int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); + + ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(nextLettersFrequencies); + boolean correctionAvailable = mSuggest.hasMinimalCorrection(); //|| mCorrectionMode == mSuggest.CORRECTION_FULL; CharSequence typedWord = mWord.getTypedWord(); diff --git a/src/com/android/inputmethod/latin/LatinKeyboard.java b/src/com/android/inputmethod/latin/LatinKeyboard.java index 64b4529f3..9b742a5f9 100644 --- a/src/com/android/inputmethod/latin/LatinKeyboard.java +++ b/src/com/android/inputmethod/latin/LatinKeyboard.java @@ -35,11 +35,15 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.inputmethodservice.Keyboard; import android.text.TextPaint; +import android.util.Log; import android.view.ViewConfiguration; import android.view.inputmethod.EditorInfo; public class LatinKeyboard extends Keyboard { + private static final boolean DEBUG_PREFERRED_LETTER = false; + private static final String TAG = "LatinKeyboard"; + private Drawable mShiftLockIcon; private Drawable mShiftLockPreviewIcon; private Drawable mOldShiftIcon; @@ -69,6 +73,12 @@ public class LatinKeyboard extends Keyboard { private boolean mCurrentlyInSpace; private SlidingLocaleDrawable mSlidingLocaleIcon; private Rect mBounds = new Rect(); + private int[] mPrefLetterFrequencies; + private boolean mPreemptiveCorrection; + private int mPrefLetter; + private int mPrefLetterX; + private int mPrefLetterY; + private int mPrefDistance; private int mExtensionResId; @@ -79,6 +89,8 @@ public class LatinKeyboard extends Keyboard { private int mShiftState = SHIFT_OFF; private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f; + private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f; + private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f; static int sSpacebarVerticalCorrection; @@ -409,9 +421,18 @@ public class LatinKeyboard extends Keyboard { return mCurrentlyInSpace; } + void setPreferredLetters(int[] frequencies) { + mPrefLetterFrequencies = frequencies; + mPrefLetter = 0; + } + void keyReleased() { mCurrentlyInSpace = false; mSpaceDragLastDiff = 0; + mPrefLetter = 0; + mPrefLetterX = 0; + mPrefLetterY = 0; + mPrefDistance = Integer.MAX_VALUE; if (mSpaceKey != null) { updateLocaleDrag(Integer.MAX_VALUE); } @@ -448,6 +469,79 @@ public class LatinKeyboard extends Keyboard { return insideSpace; } } + } else if (mPrefLetterFrequencies != null) { + // New coordinate? Reset + if (mPrefLetterX != x || mPrefLetterY != y) { + mPrefLetter = 0; + mPrefDistance = Integer.MAX_VALUE; + } + // Handle preferred next letter + final int[] pref = mPrefLetterFrequencies; + if (mPrefLetter > 0) { + if (DEBUG_PREFERRED_LETTER && mPrefLetter == code + && !key.isInsideSuper(x, y)) { + Log.d(TAG, "CORRECTED !!!!!!"); + } + return mPrefLetter == code; + } else { + final boolean inside = key.isInsideSuper(x, y); + int[] nearby = getNearestKeys(x, y); + List nearbyKeys = getKeys(); + if (inside) { + // If it's a preferred letter + if (inPrefList(code, pref)) { + // Check if its frequency is much lower than a nearby key + mPrefLetter = code; + mPrefLetterX = x; + mPrefLetterY = y; + for (int i = 0; i < nearby.length; i++) { + Key k = nearbyKeys.get(nearby[i]); + if (k != key && inPrefList(k.codes[0], pref)) { + final int dist = distanceFrom(k, x, y); + if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) && + (pref[k.codes[0]] > pref[mPrefLetter] * 3)) { + mPrefLetter = k.codes[0]; + mPrefDistance = dist; + if (DEBUG_PREFERRED_LETTER) { + Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!"); + } + break; + } + } + } + + return mPrefLetter == code; + } + } + + // Get the surrounding keys and intersect with the preferred list + // For all in the intersection + // if distance from touch point is within a reasonable distance + // make this the pref letter + // If no pref letter + // return inside; + // else return thiskey == prefletter; + + for (int i = 0; i < nearby.length; i++) { + Key k = nearbyKeys.get(nearby[i]); + if (inPrefList(k.codes[0], pref)) { + final int dist = distanceFrom(k, x, y); + if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB) + && dist < mPrefDistance) { + mPrefLetter = k.codes[0]; + mPrefLetterX = x; + mPrefLetterY = y; + mPrefDistance = dist; + } + } + } + // Didn't find any + if (mPrefLetter == 0) { + return inside; + } else { + return mPrefLetter == code; + } + } } // Lock into the spacebar @@ -456,6 +550,19 @@ public class LatinKeyboard extends Keyboard { return key.isInsideSuper(x, y); } + private boolean inPrefList(int code, int[] pref) { + if (code < pref.length && code >= 0) return pref[code] > 0; + return false; + } + + private int distanceFrom(Key k, int x, int y) { + if (y > k.y && y < k.y + k.height) { + return Math.abs(k.x + k.width / 2 - x); + } else { + return Integer.MAX_VALUE; + } + } + @Override public int[] getNearestKeys(int x, int y) { if (mCurrentlyInSpace) { @@ -512,7 +619,8 @@ public class LatinKeyboard extends Keyboard { */ @Override public boolean isInside(int x, int y) { - return LatinKeyboard.this.isInside(this, x, y); + boolean result = LatinKeyboard.this.isInside(this, x, y); + return result; } boolean isInsideSuper(int x, int y) { diff --git a/src/com/android/inputmethod/latin/LatinKeyboardView.java b/src/com/android/inputmethod/latin/LatinKeyboardView.java index 05f8aff36..bdac4a5b5 100644 --- a/src/com/android/inputmethod/latin/LatinKeyboardView.java +++ b/src/com/android/inputmethod/latin/LatinKeyboardView.java @@ -20,6 +20,7 @@ import java.util.List; import android.content.Context; import android.graphics.Canvas; +import android.graphics.Paint; import android.inputmethodservice.Keyboard; import android.inputmethodservice.KeyboardView; import android.inputmethodservice.Keyboard.Key; @@ -80,6 +81,11 @@ public class LatinKeyboardView extends KeyboardView { @Override public boolean onTouchEvent(MotionEvent me) { LatinKeyboard keyboard = (LatinKeyboard) getKeyboard(); + if (DEBUG_LINE) { + mLastX = (int) me.getX(); + mLastY = (int) me.getY(); + invalidate(); + } // Reset any bounding box controls in the keyboard if (me.getAction() == MotionEvent.ACTION_DOWN) { keyboard.keyReleased(); @@ -203,6 +209,7 @@ public class LatinKeyboardView extends KeyboardView { /**************************** INSTRUMENTATION *******************************/ static final boolean DEBUG_AUTO_PLAY = false; + static final boolean DEBUG_LINE = false; private static final int MSG_TOUCH_DOWN = 1; private static final int MSG_TOUCH_UP = 2; @@ -213,6 +220,9 @@ public class LatinKeyboardView extends KeyboardView { private boolean mDownDelivered; private Key[] mAsciiKeys = new Key[256]; private boolean mPlaying; + private int mLastX; + private int mLastY; + private Paint mPaint; @Override public void setKeyboard(Keyboard k) { @@ -309,5 +319,14 @@ public class LatinKeyboardView extends KeyboardView { mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); } } + if (DEBUG_LINE) { + if (mPaint == null) { + mPaint = new Paint(); + mPaint.setColor(0x80FFFFFF); + mPaint.setAntiAlias(false); + } + c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint); + c.drawLine(0, mLastY, getWidth(), mLastY, mPaint); + } } } diff --git a/src/com/android/inputmethod/latin/Suggest.java b/src/com/android/inputmethod/latin/Suggest.java index c3fe99635..5833c02a5 100755 --- a/src/com/android/inputmethod/latin/Suggest.java +++ b/src/com/android/inputmethod/latin/Suggest.java @@ -52,6 +52,12 @@ public class Suggest implements Dictionary.WordCallback { private int mPrefMaxSuggestions = 12; private int[] mPriorities = new int[mPrefMaxSuggestions]; + // Handle predictive correction for only the first 1280 characters for performance reasons + // If we support scripts that need latin characters beyond that, we should probably use some + // kind of a sparse array or language specific list with a mapping lookup table. + // 1280 is the size of the BASE_CHARS array in ExpandableDictionary, which is a basic set of + // latin characters. + private int[] mNextLettersFrequencies = new int[1280]; private ArrayList mSuggestions = new ArrayList(); private ArrayList mStringPool = new ArrayList(); private boolean mHaveCorrection; @@ -162,7 +168,8 @@ public class Suggest implements Dictionary.WordCallback { mCapitalize = wordComposer.isCapitalized(); collectGarbage(); Arrays.fill(mPriorities, 0); - + Arrays.fill(mNextLettersFrequencies, 0); + // Save a lowercase version of the original word mOriginalWord = wordComposer.getTypedWord(); if (mOriginalWord != null) { @@ -175,17 +182,17 @@ public class Suggest implements Dictionary.WordCallback { if (wordComposer.size() > 1) { if (mUserDictionary != null || mContactsDictionary != null) { if (mUserDictionary != null) { - mUserDictionary.getWords(wordComposer, this); + mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies); } if (mContactsDictionary != null) { - mContactsDictionary.getWords(wordComposer, this); + mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies); } if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)) { mHaveCorrection = true; } } - mMainDict.getWords(wordComposer, this); + mMainDict.getWords(wordComposer, this, mNextLettersFrequencies); if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) { mHaveCorrection = true; } @@ -229,6 +236,10 @@ public class Suggest implements Dictionary.WordCallback { return mSuggestions; } + public int[] getNextLettersFrequencies() { + return mNextLettersFrequencies; + } + private void removeDupes() { final ArrayList suggestions = mSuggestions; if (suggestions.size() < 2) return; diff --git a/src/com/android/inputmethod/latin/UserDictionary.java b/src/com/android/inputmethod/latin/UserDictionary.java index 2f3447abd..edd82aaa3 100644 --- a/src/com/android/inputmethod/latin/UserDictionary.java +++ b/src/com/android/inputmethod/latin/UserDictionary.java @@ -94,9 +94,10 @@ public class UserDictionary extends ExpandableDictionary { } @Override - public synchronized void getWords(final WordComposer codes, final WordCallback callback) { + public synchronized void getWords(final WordComposer codes, final WordCallback callback, + int[] nextLettersFrequencies) { if (mRequiresReload) loadDictionary(); - super.getWords(codes, callback); + super.getWords(codes, callback, nextLettersFrequencies); } @Override