• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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