/* * Copyright (c) 2013 Menny Even-Danan * * 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.anysoftkeyboard.dictionaries; import android.content.Context; import android.text.TextUtils; import com.anysoftkeyboard.WordComposer; import com.anysoftkeyboard.dictionaries.sqlite.AbbreviationsDictionary; import com.anysoftkeyboard.utils.IMEUtil; import com.anysoftkeyboard.utils.Log; import com.menny.android.anysoftkeyboard.R; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * This class loads a dictionary and provides a list of suggestions for a given * sequence of characters. This includes corrections and completions. */ public class Suggest implements Dictionary.WordCallback { private static final String TAG = "ASK Suggest"; private Dictionary mMainDict; private AutoText mAutoText; private int mMinimumWordSizeToStartCorrecting = 2; private final DictionaryFactory mDictionaryFactory; private Dictionary mUserDictionary; private Dictionary mAutoDictionary; private Dictionary mContactsDictionary; private Dictionary mAbbreviationDictionary; private int mPrefMaxSuggestions = 12; private final List<CharSequence> mDefaultInitialSuggestions; private List<CharSequence> mInitialSuggestions = new ArrayList<>(); private int[] mPriorities = new int[mPrefMaxSuggestions]; private List<CharSequence> mSuggestions = new ArrayList<>(); // private boolean mIncludeTypedWordIfValid; private List<CharSequence> mStringPool = new ArrayList<>(); // private Context mContext; private boolean mHaveCorrection; private CharSequence mOriginalWord; private final List<String> mExplodedAbbreviations = new ArrayList<>(); private String mLowerOriginalWord; // TODO: Remove these member variables by passing more context to addWord() // callback method private boolean mIsFirstCharCapitalized; private boolean mIsAllUpperCase; // private int mCorrectionMode = CORRECTION_FULL; private boolean mAutoTextEnabled = true; private boolean mMainDictionaryEnabled = true; public Suggest(Context context) { mDictionaryFactory = new DictionaryFactory(); for (int i = 0; i < mPrefMaxSuggestions; i++) { StringBuilder sb = new StringBuilder(32); mStringPool.add(sb); } String[] initialSuggestions = context.getResources().getStringArray( R.array.english_initial_suggestions); if (initialSuggestions != null) { mDefaultInitialSuggestions = new ArrayList<>( initialSuggestions.length); for (String suggestion : initialSuggestions) mDefaultInitialSuggestions.add(suggestion); } else { mDefaultInitialSuggestions = new ArrayList<>(0); } } public void setCorrectionMode(boolean autoText, boolean mainDictionary) { mAutoTextEnabled = autoText; mMainDictionaryEnabled = mainDictionary; } /** * 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) { if (mUserDictionary != userDictionary && mUserDictionary != null) mUserDictionary.close(); mUserDictionary = userDictionary; } public void setMainDictionary(Context askContext, DictionaryAddOnAndBuilder dictionaryBuilder) { Log.d(TAG, "Suggest: Got main dictionary! Type: " + ((dictionaryBuilder == null) ? "NULL" : dictionaryBuilder.getName())); if (mMainDict != null) { mMainDict.close(); mMainDict = null; } if (mAbbreviationDictionary != null) { mAbbreviationDictionary.close(); mAbbreviationDictionary = null; } if (dictionaryBuilder == null) { mMainDict = null; mAutoText = null; mAbbreviationDictionary = null; mInitialSuggestions = mDefaultInitialSuggestions; } else { try { System.gc(); mMainDict = dictionaryBuilder.createDictionary(); DictionaryASyncLoader loader = new DictionaryASyncLoader(null); loader.execute(mMainDict); } catch (Exception e) { e.printStackTrace(); } mAutoText = dictionaryBuilder.createAutoText(); mInitialSuggestions = dictionaryBuilder.createInitialSuggestions(); if (mInitialSuggestions == null) mInitialSuggestions = mDefaultInitialSuggestions; mAbbreviationDictionary = new AbbreviationsDictionary(askContext, dictionaryBuilder.getLanguage()); DictionaryASyncLoader loader = new DictionaryASyncLoader(null); loader.execute(mAbbreviationDictionary); } } /** * Sets an optional contacts dictionary resource to be loaded. */ public void setContactsDictionary(Context context, boolean enabled) { if (!enabled && mContactsDictionary != null) { // had one, but now config says it should be off Log.i(TAG, "Contacts dictionary has been disabled! Closing resources."); mContactsDictionary.close(); mContactsDictionary = null; } else if (enabled && mContactsDictionary == null) { // config says it should be on, but I have none. mContactsDictionary = mDictionaryFactory.createContactsDictionary(context); if (mContactsDictionary != null) {//not all devices has contacts-dictionary DictionaryASyncLoader loader = new DictionaryASyncLoader(null); loader.execute(mContactsDictionary); } } } public void setAutoDictionary(Dictionary autoDictionary) { if (mAutoDictionary != autoDictionary && mAutoDictionary != null) mAutoDictionary.close(); mAutoDictionary = autoDictionary; } /** * 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); } } private boolean haveSufficientCommonality(String original, CharSequence suggestion) { final int originalLength = original.length(); final int suggestionLength = suggestion.length(); final int lengthDiff = suggestionLength - originalLength; if (lengthDiff == 0 || lengthDiff == 1) { return true; } final int distance = IMEUtil.editDistance(original, suggestion); return distance <= 1; } public List<CharSequence> getInitialSuggestions() { return mInitialSuggestions; } /** * 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. * * @return list of suggestions. */ public List<CharSequence> getSuggestions( /* View view, */WordComposer wordComposer, boolean includeTypedWordIfValid) { mExplodedAbbreviations.clear(); mHaveCorrection = false; mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); mIsAllUpperCase = wordComposer.isAllUpperCase(); collectGarbage(); Arrays.fill(mPriorities, 0); // mIncludeTypedWordIfValid = includeTypedWordIfValid; // Save a lowercase version of the original word mOriginalWord = wordComposer.getTypedWord(); if (mOriginalWord.length() > 0) { mOriginalWord = mOriginalWord.toString(); mLowerOriginalWord = mOriginalWord.toString().toLowerCase(); } else { mLowerOriginalWord = ""; } // Search the dictionary only if there are at least 2 (configurable) // characters if (wordComposer.length() >= mMinimumWordSizeToStartCorrecting) { if (mContactsDictionary != null) { Log.v(TAG, "getSuggestions from contacts-dictionary"); mContactsDictionary.getWords(wordComposer, this); } if (mUserDictionary != null) { Log.v(TAG, "getSuggestions from user-dictionary"); mUserDictionary.getWords(wordComposer, this); } if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)) { mHaveCorrection = true; } if (mMainDict != null) { Log.v(TAG, "getSuggestions from main-dictionary"); mMainDict.getWords(wordComposer, this); } if (mAutoTextEnabled && mAbbreviationDictionary != null) { Log.v(TAG, "getSuggestions from mAbbreviationDictionary"); mAbbreviationDictionary.getWords(wordComposer, this); } if (/*mMainDictionaryEnabled &&*/ mSuggestions.size() > 0) { mHaveCorrection = true; } } if (mOriginalWord != null) { mSuggestions.add(0, mOriginalWord.toString()); } if (mExplodedAbbreviations.size() > 0) { //typed at zero, exploded at 1 index. for(String explodedWord : mExplodedAbbreviations) mSuggestions.add(1, explodedWord); mHaveCorrection = true;//so the exploded text will be auto-committed. } // Check if the first suggestion has a minimum number of characters in // common if (mMainDictionaryEnabled && mSuggestions.size() > 1 && mExplodedAbbreviations.size() == 0) { if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) { mHaveCorrection = false; } } int i = 0; int max = 6; // Don't autotext the suggestions from the dictionaries if (!mMainDictionaryEnabled && mAutoTextEnabled) max = 1; while (i < mSuggestions.size() && i < max) { String suggestedWord = mSuggestions.get(i).toString().toLowerCase(); CharSequence autoText = mAutoTextEnabled && mAutoText != null ? mAutoText .lookup(suggestedWord, 0, suggestedWord.length()) : null; // 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() && mMainDictionaryEnabled) { canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1)); } if (canAdd) { mHaveCorrection = true; mSuggestions.add(i + 1, autoText); i++; } i++; } return mSuggestions; } public boolean hasMinimalCorrection() { return mHaveCorrection; } private static boolean compareCaseInsensitive( final String lowerOriginalWord, final char[] word, final int offset, final int length) { final int originalLength = lowerOriginalWord.length(); if (originalLength == length) { for (int i = 0; i < originalLength; i++) { if (lowerOriginalWord.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, final Dictionary from) { Log.v(TAG, "Suggest::addWord"); if (from == mAbbreviationDictionary) { mExplodedAbbreviations.add(new String(word, offset, length)); return true; } 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)) { Log.v(TAG, "Suggest::addWord - forced at position 0."); 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); if (mIsAllUpperCase) { sb.append(new String(word, offset, length).toUpperCase()); } else if (mIsFirstCharCapitalized) { sb.append(Character.toUpperCase(word[offset])); if (length > 1) { sb.append(word, offset + 1, length - 1); } } else { sb.append(word, offset, length); } 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; } Log.v(TAG, "Suggest::isValidWord(%s) mMainDictionaryEnabled:%s mAutoTextEnabled: %s user-dictionary-enabled: %s contacts-dictionary-enabled: %s", word, mMainDictionaryEnabled, mAutoTextEnabled, mUserDictionary != null, mContactsDictionary != null); if (mMainDictionaryEnabled || mAutoTextEnabled) { final boolean validFromMain = (mMainDictionaryEnabled && mMainDict != null && mMainDict.isValidWord(word)); final boolean validFromUser = (mUserDictionary != null && mUserDictionary.isValidWord(word)); final boolean validFromContacts = (mContactsDictionary != null && mContactsDictionary.isValidWord(word)); Log.v(TAG, "Suggest::isValidWord(%s)validFromMain: %s validFromUser: %s validFromContacts: %s", word, validFromMain, validFromUser, validFromContacts); return validFromMain || validFromUser || /* validFromAuto || */validFromContacts; } else { return false; } } 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(TAG, "String pool got too big: " + poolSize); } mSuggestions.clear(); } public void setMinimumWordLengthForCorrection(int minLength) { // making sure it is not negative or zero mMinimumWordSizeToStartCorrecting = Math.max(1, minLength); } public DictionaryFactory getDictionaryFactory() { return mDictionaryFactory; } }