1 /* 2 * Copyright (C) 2008-2009 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.inputmethod.latin; 18 19 import android.content.Context; 20 import android.text.AutoText; 21 import android.text.TextUtils; 22 import android.util.Log; 23 import android.view.View; 24 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.List; 28 29 /** 30 * This class loads a dictionary and provides a list of suggestions for a given sequence of 31 * characters. This includes corrections and completions. 32 * @hide pending API Council Approval 33 */ 34 public class Suggest implements Dictionary.WordCallback { 35 36 public static final int CORRECTION_NONE = 0; 37 public static final int CORRECTION_BASIC = 1; 38 public static final int CORRECTION_FULL = 2; 39 40 private Dictionary mMainDict; 41 42 private Dictionary mUserDictionary; 43 44 private Dictionary mAutoDictionary; 45 46 private Dictionary mContactsDictionary; 47 48 private int mPrefMaxSuggestions = 12; 49 50 private int[] mPriorities = new int[mPrefMaxSuggestions]; 51 private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); 52 private boolean mIncludeTypedWordIfValid; 53 private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>(); 54 private Context mContext; 55 private boolean mHaveCorrection; 56 private CharSequence mOriginalWord; 57 private String mLowerOriginalWord; 58 59 private int mCorrectionMode = CORRECTION_BASIC; 60 61 Suggest(Context context, int dictionaryResId)62 public Suggest(Context context, int dictionaryResId) { 63 mContext = context; 64 mMainDict = new BinaryDictionary(context, dictionaryResId); 65 for (int i = 0; i < mPrefMaxSuggestions; i++) { 66 StringBuilder sb = new StringBuilder(32); 67 mStringPool.add(sb); 68 } 69 } 70 getCorrectionMode()71 public int getCorrectionMode() { 72 return mCorrectionMode; 73 } 74 setCorrectionMode(int mode)75 public void setCorrectionMode(int mode) { 76 mCorrectionMode = mode; 77 } 78 79 /** 80 * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted 81 * before the main dictionary, if set. 82 */ setUserDictionary(Dictionary userDictionary)83 public void setUserDictionary(Dictionary userDictionary) { 84 mUserDictionary = userDictionary; 85 } 86 87 /** 88 * Sets an optional contacts dictionary resource to be loaded. 89 */ setContactsDictionary(Dictionary userDictionary)90 public void setContactsDictionary(Dictionary userDictionary) { 91 mContactsDictionary = userDictionary; 92 } 93 setAutoDictionary(Dictionary autoDictionary)94 public void setAutoDictionary(Dictionary autoDictionary) { 95 mAutoDictionary = autoDictionary; 96 } 97 98 /** 99 * Number of suggestions to generate from the input key sequence. This has 100 * to be a number between 1 and 100 (inclusive). 101 * @param maxSuggestions 102 * @throws IllegalArgumentException if the number is out of range 103 */ setMaxSuggestions(int maxSuggestions)104 public void setMaxSuggestions(int maxSuggestions) { 105 if (maxSuggestions < 1 || maxSuggestions > 100) { 106 throw new IllegalArgumentException("maxSuggestions must be between 1 and 100"); 107 } 108 mPrefMaxSuggestions = maxSuggestions; 109 mPriorities = new int[mPrefMaxSuggestions]; 110 collectGarbage(); 111 while (mStringPool.size() < mPrefMaxSuggestions) { 112 StringBuilder sb = new StringBuilder(32); 113 mStringPool.add(sb); 114 } 115 } 116 haveSufficientCommonality(String original, CharSequence suggestion)117 private boolean haveSufficientCommonality(String original, CharSequence suggestion) { 118 final int originalLength = original.length(); 119 final int suggestionLength = suggestion.length(); 120 final int minLength = Math.min(originalLength, suggestionLength); 121 if (minLength <= 2) return true; 122 int matching = 0; 123 int lessMatching = 0; // Count matches if we skip one character 124 int i; 125 for (i = 0; i < minLength; i++) { 126 final char origChar = ExpandableDictionary.toLowerCase(original.charAt(i)); 127 if (origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i))) { 128 matching++; 129 lessMatching++; 130 } else if (i + 1 < suggestionLength 131 && origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i + 1))) { 132 lessMatching++; 133 } 134 } 135 matching = Math.max(matching, lessMatching); 136 137 if (minLength <= 4) { 138 return matching >= 2; 139 } else { 140 return matching > minLength / 2; 141 } 142 } 143 144 /** 145 * Returns a list of words that match the list of character codes passed in. 146 * This list will be overwritten the next time this function is called. 147 * @param a view for retrieving the context for AutoText 148 * @param codes the list of codes. Each list item contains an array of character codes 149 * in order of probability where the character at index 0 in the array has the highest 150 * probability. 151 * @return list of suggestions. 152 */ getSuggestions(View view, WordComposer wordComposer, boolean includeTypedWordIfValid)153 public List<CharSequence> getSuggestions(View view, WordComposer wordComposer, 154 boolean includeTypedWordIfValid) { 155 mHaveCorrection = false; 156 collectGarbage(); 157 Arrays.fill(mPriorities, 0); 158 mIncludeTypedWordIfValid = includeTypedWordIfValid; 159 160 // Save a lowercase version of the original word 161 mOriginalWord = wordComposer.getTypedWord(); 162 if (mOriginalWord != null) { 163 mOriginalWord = mOriginalWord.toString(); 164 mLowerOriginalWord = mOriginalWord.toString().toLowerCase(); 165 } else { 166 mLowerOriginalWord = ""; 167 } 168 // Search the dictionary only if there are at least 2 characters 169 if (wordComposer.size() > 1) { 170 if (mUserDictionary != null || mContactsDictionary != null) { 171 if (mUserDictionary != null) { 172 mUserDictionary.getWords(wordComposer, this); 173 } 174 if (mContactsDictionary != null) { 175 mContactsDictionary.getWords(wordComposer, this); 176 } 177 178 if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)) { 179 mHaveCorrection = true; 180 } 181 } 182 mMainDict.getWords(wordComposer, this); 183 if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) { 184 mHaveCorrection = true; 185 } 186 } 187 if (mOriginalWord != null) { 188 mSuggestions.add(0, mOriginalWord.toString()); 189 } 190 191 // Check if the first suggestion has a minimum number of characters in common 192 if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 1) { 193 if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) { 194 mHaveCorrection = false; 195 } 196 } 197 198 int i = 0; 199 int max = 6; 200 // Don't autotext the suggestions from the dictionaries 201 if (mCorrectionMode == CORRECTION_BASIC) max = 1; 202 while (i < mSuggestions.size() && i < max) { 203 String suggestedWord = mSuggestions.get(i).toString().toLowerCase(); 204 CharSequence autoText = 205 AutoText.get(suggestedWord, 0, suggestedWord.length(), view); 206 // Is there an AutoText correction? 207 boolean canAdd = autoText != null; 208 // Is that correction already the current prediction (or original word)? 209 canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i)); 210 // Is that correction already the next predicted word? 211 if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) { 212 canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1)); 213 } 214 if (canAdd) { 215 mHaveCorrection = true; 216 mSuggestions.add(i + 1, autoText); 217 i++; 218 } 219 i++; 220 } 221 222 removeDupes(); 223 return mSuggestions; 224 } 225 removeDupes()226 private void removeDupes() { 227 final ArrayList<CharSequence> suggestions = mSuggestions; 228 if (suggestions.size() < 2) return; 229 int i = 1; 230 // Don't cache suggestions.size(), since we may be removing items 231 while (i < suggestions.size()) { 232 final CharSequence cur = suggestions.get(i); 233 // Compare each candidate with each previous candidate 234 for (int j = 0; j < i; j++) { 235 CharSequence previous = suggestions.get(j); 236 if (TextUtils.equals(cur, previous)) { 237 removeFromSuggestions(i); 238 i--; 239 break; 240 } 241 } 242 i++; 243 } 244 } 245 removeFromSuggestions(int index)246 private void removeFromSuggestions(int index) { 247 CharSequence garbage = mSuggestions.remove(index); 248 if (garbage != null && garbage instanceof StringBuilder) { 249 mStringPool.add(garbage); 250 } 251 } 252 hasMinimalCorrection()253 public boolean hasMinimalCorrection() { 254 return mHaveCorrection; 255 } 256 compareCaseInsensitive(final String mLowerOriginalWord, final char[] word, final int offset, final int length)257 private boolean compareCaseInsensitive(final String mLowerOriginalWord, 258 final char[] word, final int offset, final int length) { 259 final int originalLength = mLowerOriginalWord.length(); 260 if (originalLength == length && Character.isUpperCase(word[offset])) { 261 for (int i = 0; i < originalLength; i++) { 262 if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) { 263 return false; 264 } 265 } 266 return true; 267 } 268 return false; 269 } 270 addWord(final char[] word, final int offset, final int length, final int freq)271 public boolean addWord(final char[] word, final int offset, final int length, final int freq) { 272 int pos = 0; 273 final int[] priorities = mPriorities; 274 final int prefMaxSuggestions = mPrefMaxSuggestions; 275 // Check if it's the same word, only caps are different 276 if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) { 277 pos = 0; 278 } else { 279 // Check the last one's priority and bail 280 if (priorities[prefMaxSuggestions - 1] >= freq) return true; 281 while (pos < prefMaxSuggestions) { 282 if (priorities[pos] < freq 283 || (priorities[pos] == freq && length < mSuggestions 284 .get(pos).length())) { 285 break; 286 } 287 pos++; 288 } 289 } 290 291 if (pos >= prefMaxSuggestions) { 292 return true; 293 } 294 System.arraycopy(priorities, pos, priorities, pos + 1, 295 prefMaxSuggestions - pos - 1); 296 priorities[pos] = freq; 297 int poolSize = mStringPool.size(); 298 StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) 299 : new StringBuilder(32); 300 sb.setLength(0); 301 sb.append(word, offset, length); 302 mSuggestions.add(pos, sb); 303 if (mSuggestions.size() > prefMaxSuggestions) { 304 CharSequence garbage = mSuggestions.remove(prefMaxSuggestions); 305 if (garbage instanceof StringBuilder) { 306 mStringPool.add(garbage); 307 } 308 } 309 return true; 310 } 311 isValidWord(final CharSequence word)312 public boolean isValidWord(final CharSequence word) { 313 if (word == null || word.length() == 0) { 314 return false; 315 } 316 return (mCorrectionMode == CORRECTION_FULL && mMainDict.isValidWord(word)) 317 || (mCorrectionMode > CORRECTION_NONE && 318 ((mUserDictionary != null && mUserDictionary.isValidWord(word))) 319 || (mAutoDictionary != null && mAutoDictionary.isValidWord(word)) 320 || (mContactsDictionary != null && mContactsDictionary.isValidWord(word))); 321 } 322 collectGarbage()323 private void collectGarbage() { 324 int poolSize = mStringPool.size(); 325 int garbageSize = mSuggestions.size(); 326 while (poolSize < mPrefMaxSuggestions && garbageSize > 0) { 327 CharSequence garbage = mSuggestions.get(garbageSize - 1); 328 if (garbage != null && garbage instanceof StringBuilder) { 329 mStringPool.add(garbage); 330 poolSize++; 331 } 332 garbageSize--; 333 } 334 if (poolSize == mPrefMaxSuggestions + 1) { 335 Log.w("Suggest", "String pool got too big: " + poolSize); 336 } 337 mSuggestions.clear(); 338 } 339 } 340