2009-03-13 22:11:42 +00:00
|
|
|
/*
|
2010-03-26 22:07:10 +00:00
|
|
|
* Copyright (C) 2008 The Android Open Source Project
|
2009-03-13 22:11:42 +00:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
|
|
* use this file except in compliance with the License. You may obtain a copy of
|
|
|
|
* the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
* License for the specific language governing permissions and limitations under
|
|
|
|
* the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package com.android.inputmethod.latin;
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
import android.text.AutoText;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Log;
|
|
|
|
import android.view.View;
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.List;
|
|
|
|
|
2010-01-16 02:21:58 +00:00
|
|
|
import com.android.inputmethod.latin.WordComposer;
|
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
/**
|
|
|
|
* This class loads a dictionary and provides a list of suggestions for a given sequence of
|
|
|
|
* characters. This includes corrections and completions.
|
|
|
|
* @hide pending API Council Approval
|
|
|
|
*/
|
|
|
|
public class Suggest implements Dictionary.WordCallback {
|
|
|
|
|
|
|
|
public static final int CORRECTION_NONE = 0;
|
|
|
|
public static final int CORRECTION_BASIC = 1;
|
|
|
|
public static final int CORRECTION_FULL = 2;
|
2009-07-17 19:51:45 +00:00
|
|
|
|
2010-03-17 22:36:23 +00:00
|
|
|
static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000;
|
2009-12-18 21:39:18 +00:00
|
|
|
|
|
|
|
private BinaryDictionary mMainDict;
|
2009-07-17 19:51:45 +00:00
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
private Dictionary mUserDictionary;
|
2009-07-17 19:51:45 +00:00
|
|
|
|
|
|
|
private Dictionary mAutoDictionary;
|
|
|
|
|
2009-07-28 23:48:47 +00:00
|
|
|
private Dictionary mContactsDictionary;
|
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
private int mPrefMaxSuggestions = 12;
|
2009-07-17 19:51:45 +00:00
|
|
|
|
2010-02-18 19:09:43 +00:00
|
|
|
private boolean mAutoTextEnabled;
|
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
private int[] mPriorities = new int[mPrefMaxSuggestions];
|
2010-02-05 22:07:04 +00:00
|
|
|
// 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];
|
2009-10-28 01:08:19 +00:00
|
|
|
private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
|
|
|
|
private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
|
2009-03-13 22:11:42 +00:00
|
|
|
private boolean mHaveCorrection;
|
|
|
|
private CharSequence mOriginalWord;
|
|
|
|
private String mLowerOriginalWord;
|
2010-01-16 02:21:58 +00:00
|
|
|
private boolean mCapitalize;
|
2009-03-13 22:11:42 +00:00
|
|
|
|
|
|
|
private int mCorrectionMode = CORRECTION_BASIC;
|
|
|
|
|
|
|
|
|
|
|
|
public Suggest(Context context, int dictionaryResId) {
|
|
|
|
mMainDict = new BinaryDictionary(context, dictionaryResId);
|
|
|
|
for (int i = 0; i < mPrefMaxSuggestions; i++) {
|
|
|
|
StringBuilder sb = new StringBuilder(32);
|
|
|
|
mStringPool.add(sb);
|
|
|
|
}
|
|
|
|
}
|
2009-07-17 19:51:45 +00:00
|
|
|
|
2010-02-18 19:09:43 +00:00
|
|
|
public void setAutoTextEnabled(boolean enabled) {
|
|
|
|
mAutoTextEnabled = enabled;
|
|
|
|
}
|
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
public int getCorrectionMode() {
|
|
|
|
return mCorrectionMode;
|
|
|
|
}
|
2009-07-17 19:51:45 +00:00
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
public void setCorrectionMode(int mode) {
|
|
|
|
mCorrectionMode = mode;
|
|
|
|
}
|
|
|
|
|
2009-12-18 21:39:18 +00:00
|
|
|
public boolean hasMainDictionary() {
|
|
|
|
return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD;
|
|
|
|
}
|
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
/**
|
|
|
|
* Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
|
|
|
|
* before the main dictionary, if set.
|
|
|
|
*/
|
|
|
|
public void setUserDictionary(Dictionary userDictionary) {
|
|
|
|
mUserDictionary = userDictionary;
|
|
|
|
}
|
2009-07-28 23:48:47 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets an optional contacts dictionary resource to be loaded.
|
|
|
|
*/
|
|
|
|
public void setContactsDictionary(Dictionary userDictionary) {
|
|
|
|
mContactsDictionary = userDictionary;
|
|
|
|
}
|
2009-07-17 19:51:45 +00:00
|
|
|
|
|
|
|
public void setAutoDictionary(Dictionary autoDictionary) {
|
|
|
|
mAutoDictionary = autoDictionary;
|
|
|
|
}
|
2009-03-13 22:11:42 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of suggestions to generate from the input key sequence. This has
|
|
|
|
* to be a number between 1 and 100 (inclusive).
|
|
|
|
* @param maxSuggestions
|
|
|
|
* @throws IllegalArgumentException if the number is out of range
|
|
|
|
*/
|
|
|
|
public void setMaxSuggestions(int maxSuggestions) {
|
|
|
|
if (maxSuggestions < 1 || maxSuggestions > 100) {
|
|
|
|
throw new IllegalArgumentException("maxSuggestions must be between 1 and 100");
|
|
|
|
}
|
|
|
|
mPrefMaxSuggestions = maxSuggestions;
|
|
|
|
mPriorities = new int[mPrefMaxSuggestions];
|
|
|
|
collectGarbage();
|
|
|
|
while (mStringPool.size() < mPrefMaxSuggestions) {
|
|
|
|
StringBuilder sb = new StringBuilder(32);
|
|
|
|
mStringPool.add(sb);
|
|
|
|
}
|
|
|
|
}
|
2009-07-30 17:11:33 +00:00
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
private boolean haveSufficientCommonality(String original, CharSequence suggestion) {
|
2009-07-30 17:11:33 +00:00
|
|
|
final int originalLength = original.length();
|
|
|
|
final int suggestionLength = suggestion.length();
|
|
|
|
final int minLength = Math.min(originalLength, suggestionLength);
|
|
|
|
if (minLength <= 2) return true;
|
2009-03-13 22:11:42 +00:00
|
|
|
int matching = 0;
|
2009-07-30 17:11:33 +00:00
|
|
|
int lessMatching = 0; // Count matches if we skip one character
|
|
|
|
int i;
|
|
|
|
for (i = 0; i < minLength; i++) {
|
|
|
|
final char origChar = ExpandableDictionary.toLowerCase(original.charAt(i));
|
|
|
|
if (origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i))) {
|
2009-03-13 22:11:42 +00:00
|
|
|
matching++;
|
2009-07-30 17:11:33 +00:00
|
|
|
lessMatching++;
|
|
|
|
} else if (i + 1 < suggestionLength
|
|
|
|
&& origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i + 1))) {
|
|
|
|
lessMatching++;
|
2009-03-13 22:11:42 +00:00
|
|
|
}
|
|
|
|
}
|
2009-07-30 17:11:33 +00:00
|
|
|
matching = Math.max(matching, lessMatching);
|
|
|
|
|
|
|
|
if (minLength <= 4) {
|
2009-03-13 22:11:42 +00:00
|
|
|
return matching >= 2;
|
|
|
|
} else {
|
2009-07-30 17:11:33 +00:00
|
|
|
return matching > minLength / 2;
|
2009-03-13 22:11:42 +00:00
|
|
|
}
|
|
|
|
}
|
2009-07-30 17:11:33 +00:00
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
/**
|
|
|
|
* Returns a list of words that match the list of character codes passed in.
|
|
|
|
* This list will be overwritten the next time this function is called.
|
|
|
|
* @param a view for retrieving the context for AutoText
|
|
|
|
* @param codes the list of codes. Each list item contains an array of character codes
|
|
|
|
* in order of probability where the character at index 0 in the array has the highest
|
|
|
|
* probability.
|
|
|
|
* @return list of suggestions.
|
|
|
|
*/
|
|
|
|
public List<CharSequence> getSuggestions(View view, WordComposer wordComposer,
|
|
|
|
boolean includeTypedWordIfValid) {
|
|
|
|
mHaveCorrection = false;
|
2010-01-16 02:21:58 +00:00
|
|
|
mCapitalize = wordComposer.isCapitalized();
|
2009-03-13 22:11:42 +00:00
|
|
|
collectGarbage();
|
|
|
|
Arrays.fill(mPriorities, 0);
|
2010-02-05 22:07:04 +00:00
|
|
|
Arrays.fill(mNextLettersFrequencies, 0);
|
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
// Save a lowercase version of the original word
|
|
|
|
mOriginalWord = wordComposer.getTypedWord();
|
|
|
|
if (mOriginalWord != null) {
|
|
|
|
mOriginalWord = mOriginalWord.toString();
|
|
|
|
mLowerOriginalWord = mOriginalWord.toString().toLowerCase();
|
|
|
|
} else {
|
|
|
|
mLowerOriginalWord = "";
|
|
|
|
}
|
|
|
|
// Search the dictionary only if there are at least 2 characters
|
|
|
|
if (wordComposer.size() > 1) {
|
2009-07-28 23:48:47 +00:00
|
|
|
if (mUserDictionary != null || mContactsDictionary != null) {
|
|
|
|
if (mUserDictionary != null) {
|
2010-02-05 22:07:04 +00:00
|
|
|
mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
|
2009-07-28 23:48:47 +00:00
|
|
|
}
|
|
|
|
if (mContactsDictionary != null) {
|
2010-02-05 22:07:04 +00:00
|
|
|
mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
|
2009-07-28 23:48:47 +00:00
|
|
|
}
|
|
|
|
|
2010-02-20 00:38:58 +00:00
|
|
|
if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)
|
|
|
|
&& mCorrectionMode == CORRECTION_FULL) {
|
2009-03-13 22:11:42 +00:00
|
|
|
mHaveCorrection = true;
|
|
|
|
}
|
|
|
|
}
|
2010-02-05 22:07:04 +00:00
|
|
|
mMainDict.getWords(wordComposer, this, mNextLettersFrequencies);
|
2009-03-13 22:11:42 +00:00
|
|
|
if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) {
|
|
|
|
mHaveCorrection = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (mOriginalWord != null) {
|
|
|
|
mSuggestions.add(0, mOriginalWord.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the first suggestion has a minimum number of characters in common
|
|
|
|
if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 1) {
|
|
|
|
if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) {
|
|
|
|
mHaveCorrection = false;
|
|
|
|
}
|
|
|
|
}
|
2010-02-18 19:09:43 +00:00
|
|
|
|
|
|
|
if (mAutoTextEnabled) {
|
|
|
|
int i = 0;
|
|
|
|
int max = 6;
|
|
|
|
// Don't autotext the suggestions from the dictionaries
|
|
|
|
if (mCorrectionMode == CORRECTION_BASIC) max = 1;
|
|
|
|
while (i < mSuggestions.size() && i < max) {
|
|
|
|
String suggestedWord = mSuggestions.get(i).toString().toLowerCase();
|
|
|
|
CharSequence autoText =
|
|
|
|
AutoText.get(suggestedWord, 0, suggestedWord.length(), view);
|
|
|
|
// Is there an AutoText correction?
|
|
|
|
boolean canAdd = autoText != null;
|
|
|
|
// Is that correction already the current prediction (or original word)?
|
|
|
|
canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i));
|
|
|
|
// Is that correction already the next predicted word?
|
|
|
|
if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) {
|
|
|
|
canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1));
|
|
|
|
}
|
|
|
|
if (canAdd) {
|
|
|
|
mHaveCorrection = true;
|
|
|
|
mSuggestions.add(i + 1, autoText);
|
|
|
|
i++;
|
|
|
|
}
|
2009-03-13 22:11:42 +00:00
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
2009-10-28 01:08:19 +00:00
|
|
|
|
|
|
|
removeDupes();
|
2009-03-13 22:11:42 +00:00
|
|
|
return mSuggestions;
|
|
|
|
}
|
|
|
|
|
2010-02-05 22:07:04 +00:00
|
|
|
public int[] getNextLettersFrequencies() {
|
|
|
|
return mNextLettersFrequencies;
|
|
|
|
}
|
|
|
|
|
2009-10-28 01:08:19 +00:00
|
|
|
private void removeDupes() {
|
|
|
|
final ArrayList<CharSequence> suggestions = mSuggestions;
|
|
|
|
if (suggestions.size() < 2) return;
|
|
|
|
int i = 1;
|
|
|
|
// Don't cache suggestions.size(), since we may be removing items
|
|
|
|
while (i < suggestions.size()) {
|
|
|
|
final CharSequence cur = suggestions.get(i);
|
|
|
|
// Compare each candidate with each previous candidate
|
|
|
|
for (int j = 0; j < i; j++) {
|
|
|
|
CharSequence previous = suggestions.get(j);
|
|
|
|
if (TextUtils.equals(cur, previous)) {
|
|
|
|
removeFromSuggestions(i);
|
|
|
|
i--;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void removeFromSuggestions(int index) {
|
|
|
|
CharSequence garbage = mSuggestions.remove(index);
|
|
|
|
if (garbage != null && garbage instanceof StringBuilder) {
|
|
|
|
mStringPool.add(garbage);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2009-03-13 22:11:42 +00:00
|
|
|
public boolean hasMinimalCorrection() {
|
|
|
|
return mHaveCorrection;
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean compareCaseInsensitive(final String mLowerOriginalWord,
|
|
|
|
final char[] word, final int offset, final int length) {
|
|
|
|
final int originalLength = mLowerOriginalWord.length();
|
|
|
|
if (originalLength == length && Character.isUpperCase(word[offset])) {
|
|
|
|
for (int i = 0; i < originalLength; i++) {
|
|
|
|
if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean addWord(final char[] word, final int offset, final int length, final int freq) {
|
|
|
|
int pos = 0;
|
|
|
|
final int[] priorities = mPriorities;
|
|
|
|
final int prefMaxSuggestions = mPrefMaxSuggestions;
|
|
|
|
// Check if it's the same word, only caps are different
|
|
|
|
if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) {
|
|
|
|
pos = 0;
|
|
|
|
} else {
|
|
|
|
// Check the last one's priority and bail
|
|
|
|
if (priorities[prefMaxSuggestions - 1] >= freq) return true;
|
|
|
|
while (pos < prefMaxSuggestions) {
|
|
|
|
if (priorities[pos] < freq
|
|
|
|
|| (priorities[pos] == freq && length < mSuggestions
|
|
|
|
.get(pos).length())) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
pos++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pos >= prefMaxSuggestions) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
System.arraycopy(priorities, pos, priorities, pos + 1,
|
|
|
|
prefMaxSuggestions - pos - 1);
|
|
|
|
priorities[pos] = freq;
|
|
|
|
int poolSize = mStringPool.size();
|
|
|
|
StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
|
|
|
|
: new StringBuilder(32);
|
|
|
|
sb.setLength(0);
|
2010-01-16 02:21:58 +00:00
|
|
|
if (mCapitalize) {
|
|
|
|
sb.append(Character.toUpperCase(word[offset]));
|
|
|
|
if (length > 1) {
|
|
|
|
sb.append(word, offset + 1, length - 1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
sb.append(word, offset, length);
|
|
|
|
}
|
2009-03-13 22:11:42 +00:00
|
|
|
mSuggestions.add(pos, sb);
|
|
|
|
if (mSuggestions.size() > prefMaxSuggestions) {
|
|
|
|
CharSequence garbage = mSuggestions.remove(prefMaxSuggestions);
|
|
|
|
if (garbage instanceof StringBuilder) {
|
|
|
|
mStringPool.add(garbage);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isValidWord(final CharSequence word) {
|
|
|
|
if (word == null || word.length() == 0) {
|
|
|
|
return false;
|
|
|
|
}
|
2010-03-24 23:44:31 +00:00
|
|
|
return mMainDict.isValidWord(word)
|
|
|
|
|| (mUserDictionary != null && mUserDictionary.isValidWord(word))
|
|
|
|
|| (mAutoDictionary != null && mAutoDictionary.isValidWord(word))
|
|
|
|
|| (mContactsDictionary != null && mContactsDictionary.isValidWord(word));
|
2009-03-13 22:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void collectGarbage() {
|
|
|
|
int poolSize = mStringPool.size();
|
|
|
|
int garbageSize = mSuggestions.size();
|
|
|
|
while (poolSize < mPrefMaxSuggestions && garbageSize > 0) {
|
|
|
|
CharSequence garbage = mSuggestions.get(garbageSize - 1);
|
|
|
|
if (garbage != null && garbage instanceof StringBuilder) {
|
|
|
|
mStringPool.add(garbage);
|
|
|
|
poolSize++;
|
|
|
|
}
|
|
|
|
garbageSize--;
|
|
|
|
}
|
|
|
|
if (poolSize == mPrefMaxSuggestions + 1) {
|
|
|
|
Log.w("Suggest", "String pool got too big: " + poolSize);
|
|
|
|
}
|
|
|
|
mSuggestions.clear();
|
|
|
|
}
|
2009-10-12 20:48:35 +00:00
|
|
|
|
|
|
|
public void close() {
|
|
|
|
if (mMainDict != null) {
|
|
|
|
mMainDict.close();
|
|
|
|
}
|
|
|
|
}
|
2009-03-13 22:11:42 +00:00
|
|
|
}
|