• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
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.nio.ByteBuffer;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.List;
29 
30 /**
31  * This class loads a dictionary and provides a list of suggestions for a given sequence of
32  * characters. This includes corrections and completions.
33  * @hide pending API Council Approval
34  */
35 public class Suggest implements Dictionary.WordCallback {
36 
37     public static final int APPROX_MAX_WORD_LENGTH = 32;
38 
39     public static final int CORRECTION_NONE = 0;
40     public static final int CORRECTION_BASIC = 1;
41     public static final int CORRECTION_FULL = 2;
42     public static final int CORRECTION_FULL_BIGRAM = 3;
43 
44     /**
45      * Words that appear in both bigram and unigram data gets multiplier ranging from
46      * BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the frequency score from
47      * bigram data.
48      */
49     public static final double BIGRAM_MULTIPLIER_MIN = 1.2;
50     public static final double BIGRAM_MULTIPLIER_MAX = 1.5;
51 
52     /**
53      * Maximum possible bigram frequency. Will depend on how many bits are being used in data
54      * structure. Maximum bigram freqeuncy will get the BIGRAM_MULTIPLIER_MAX as the multiplier.
55      */
56     public static final int MAXIMUM_BIGRAM_FREQUENCY = 127;
57 
58     public static final int DIC_USER_TYPED = 0;
59     public static final int DIC_MAIN = 1;
60     public static final int DIC_USER = 2;
61     public static final int DIC_AUTO = 3;
62     public static final int DIC_CONTACTS = 4;
63     // If you add a type of dictionary, increment DIC_TYPE_LAST_ID
64     public static final int DIC_TYPE_LAST_ID = 4;
65 
66     static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000;
67 
68     private BinaryDictionary mMainDict;
69 
70     private Dictionary mUserDictionary;
71 
72     private Dictionary mAutoDictionary;
73 
74     private Dictionary mContactsDictionary;
75 
76     private Dictionary mUserBigramDictionary;
77 
78     private int mPrefMaxSuggestions = 12;
79 
80     private static final int PREF_MAX_BIGRAMS = 60;
81 
82     private boolean mAutoTextEnabled;
83 
84     private int[] mPriorities = new int[mPrefMaxSuggestions];
85     private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS];
86 
87     // Handle predictive correction for only the first 1280 characters for performance reasons
88     // If we support scripts that need latin characters beyond that, we should probably use some
89     // kind of a sparse array or language specific list with a mapping lookup table.
90     // 1280 is the size of the BASE_CHARS array in ExpandableDictionary, which is a basic set of
91     // latin characters.
92     private int[] mNextLettersFrequencies = new int[1280];
93     private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
94     ArrayList<CharSequence> mBigramSuggestions  = new ArrayList<CharSequence>();
95     private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
96     private boolean mHaveCorrection;
97     private CharSequence mOriginalWord;
98     private String mLowerOriginalWord;
99 
100     // TODO: Remove these member variables by passing more context to addWord() callback method
101     private boolean mIsFirstCharCapitalized;
102     private boolean mIsAllUpperCase;
103 
104     private int mCorrectionMode = CORRECTION_BASIC;
105 
Suggest(Context context, int[] dictionaryResId)106     public Suggest(Context context, int[] dictionaryResId) {
107         mMainDict = new BinaryDictionary(context, dictionaryResId, DIC_MAIN);
108         initPool();
109     }
110 
Suggest(Context context, ByteBuffer byteBuffer)111     public Suggest(Context context, ByteBuffer byteBuffer) {
112         mMainDict = new BinaryDictionary(context, byteBuffer, DIC_MAIN);
113         initPool();
114     }
115 
initPool()116     private void initPool() {
117         for (int i = 0; i < mPrefMaxSuggestions; i++) {
118             StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
119             mStringPool.add(sb);
120         }
121     }
122 
setAutoTextEnabled(boolean enabled)123     public void setAutoTextEnabled(boolean enabled) {
124         mAutoTextEnabled = enabled;
125     }
126 
getCorrectionMode()127     public int getCorrectionMode() {
128         return mCorrectionMode;
129     }
130 
setCorrectionMode(int mode)131     public void setCorrectionMode(int mode) {
132         mCorrectionMode = mode;
133     }
134 
hasMainDictionary()135     public boolean hasMainDictionary() {
136         return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD;
137     }
138 
getApproxMaxWordLength()139     public int getApproxMaxWordLength() {
140         return APPROX_MAX_WORD_LENGTH;
141     }
142 
143     /**
144      * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
145      * before the main dictionary, if set.
146      */
setUserDictionary(Dictionary userDictionary)147     public void setUserDictionary(Dictionary userDictionary) {
148         mUserDictionary = userDictionary;
149     }
150 
151     /**
152      * Sets an optional contacts dictionary resource to be loaded.
153      */
setContactsDictionary(Dictionary userDictionary)154     public void setContactsDictionary(Dictionary userDictionary) {
155         mContactsDictionary = userDictionary;
156     }
157 
setAutoDictionary(Dictionary autoDictionary)158     public void setAutoDictionary(Dictionary autoDictionary) {
159         mAutoDictionary = autoDictionary;
160     }
161 
setUserBigramDictionary(Dictionary userBigramDictionary)162     public void setUserBigramDictionary(Dictionary userBigramDictionary) {
163         mUserBigramDictionary = userBigramDictionary;
164     }
165 
166     /**
167      * Number of suggestions to generate from the input key sequence. This has
168      * to be a number between 1 and 100 (inclusive).
169      * @param maxSuggestions
170      * @throws IllegalArgumentException if the number is out of range
171      */
setMaxSuggestions(int maxSuggestions)172     public void setMaxSuggestions(int maxSuggestions) {
173         if (maxSuggestions < 1 || maxSuggestions > 100) {
174             throw new IllegalArgumentException("maxSuggestions must be between 1 and 100");
175         }
176         mPrefMaxSuggestions = maxSuggestions;
177         mPriorities = new int[mPrefMaxSuggestions];
178         mBigramPriorities = new int[PREF_MAX_BIGRAMS];
179         collectGarbage(mSuggestions, mPrefMaxSuggestions);
180         while (mStringPool.size() < mPrefMaxSuggestions) {
181             StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
182             mStringPool.add(sb);
183         }
184     }
185 
haveSufficientCommonality(String original, CharSequence suggestion)186     private boolean haveSufficientCommonality(String original, CharSequence suggestion) {
187         final int originalLength = original.length();
188         final int suggestionLength = suggestion.length();
189         final int minLength = Math.min(originalLength, suggestionLength);
190         if (minLength <= 2) return true;
191         int matching = 0;
192         int lessMatching = 0; // Count matches if we skip one character
193         int i;
194         for (i = 0; i < minLength; i++) {
195             final char origChar = ExpandableDictionary.toLowerCase(original.charAt(i));
196             if (origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i))) {
197                 matching++;
198                 lessMatching++;
199             } else if (i + 1 < suggestionLength
200                     && origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i + 1))) {
201                 lessMatching++;
202             }
203         }
204         matching = Math.max(matching, lessMatching);
205 
206         if (minLength <= 4) {
207             return matching >= 2;
208         } else {
209             return matching > minLength / 2;
210         }
211     }
212 
213     /**
214      * Returns a list of words that match the list of character codes passed in.
215      * This list will be overwritten the next time this function is called.
216      * @param view a view for retrieving the context for AutoText
217      * @param wordComposer contains what is currently being typed
218      * @param prevWordForBigram previous word (used only for bigram)
219      * @return list of suggestions.
220      */
getSuggestions(View view, WordComposer wordComposer, boolean includeTypedWordIfValid, CharSequence prevWordForBigram)221     public List<CharSequence> getSuggestions(View view, WordComposer wordComposer,
222             boolean includeTypedWordIfValid, CharSequence prevWordForBigram) {
223         LatinImeLogger.onStartSuggestion(prevWordForBigram);
224         mHaveCorrection = false;
225         mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
226         mIsAllUpperCase = wordComposer.isAllUpperCase();
227         collectGarbage(mSuggestions, mPrefMaxSuggestions);
228         Arrays.fill(mPriorities, 0);
229         Arrays.fill(mNextLettersFrequencies, 0);
230 
231         // Save a lowercase version of the original word
232         mOriginalWord = wordComposer.getTypedWord();
233         if (mOriginalWord != null) {
234             final String mOriginalWordString = mOriginalWord.toString();
235             mOriginalWord = mOriginalWordString;
236             mLowerOriginalWord = mOriginalWordString.toLowerCase();
237             // Treating USER_TYPED as UNIGRAM suggestion for logging now.
238             LatinImeLogger.onAddSuggestedWord(mOriginalWordString, Suggest.DIC_USER_TYPED,
239                     Dictionary.DataType.UNIGRAM);
240         } else {
241             mLowerOriginalWord = "";
242         }
243 
244         if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
245                 || mCorrectionMode == CORRECTION_BASIC)) {
246             // At first character typed, search only the bigrams
247             Arrays.fill(mBigramPriorities, 0);
248             collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS);
249 
250             if (!TextUtils.isEmpty(prevWordForBigram)) {
251                 CharSequence lowerPrevWord = prevWordForBigram.toString().toLowerCase();
252                 if (mMainDict.isValidWord(lowerPrevWord)) {
253                     prevWordForBigram = lowerPrevWord;
254                 }
255                 if (mUserBigramDictionary != null) {
256                     mUserBigramDictionary.getBigrams(wordComposer, prevWordForBigram, this,
257                             mNextLettersFrequencies);
258                 }
259                 if (mContactsDictionary != null) {
260                     mContactsDictionary.getBigrams(wordComposer, prevWordForBigram, this,
261                             mNextLettersFrequencies);
262                 }
263                 if (mMainDict != null) {
264                     mMainDict.getBigrams(wordComposer, prevWordForBigram, this,
265                             mNextLettersFrequencies);
266                 }
267                 char currentChar = wordComposer.getTypedWord().charAt(0);
268                 // TODO: Must pay attention to locale when changing case.
269                 char currentCharUpper = Character.toUpperCase(currentChar);
270                 int count = 0;
271                 int bigramSuggestionSize = mBigramSuggestions.size();
272                 for (int i = 0; i < bigramSuggestionSize; i++) {
273                     if (mBigramSuggestions.get(i).charAt(0) == currentChar
274                             || mBigramSuggestions.get(i).charAt(0) == currentCharUpper) {
275                         int poolSize = mStringPool.size();
276                         StringBuilder sb = poolSize > 0 ?
277                                 (StringBuilder) mStringPool.remove(poolSize - 1)
278                                 : new StringBuilder(getApproxMaxWordLength());
279                         sb.setLength(0);
280                         sb.append(mBigramSuggestions.get(i));
281                         mSuggestions.add(count++, sb);
282                         if (count > mPrefMaxSuggestions) break;
283                     }
284                 }
285             }
286 
287         } else if (wordComposer.size() > 1) {
288             // At second character typed, search the unigrams (scores being affected by bigrams)
289             if (mUserDictionary != null || mContactsDictionary != null) {
290                 if (mUserDictionary != null) {
291                     mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
292                 }
293                 if (mContactsDictionary != null) {
294                     mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
295                 }
296 
297                 if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)
298                         && (mCorrectionMode == CORRECTION_FULL
299                         || mCorrectionMode == CORRECTION_FULL_BIGRAM)) {
300                     mHaveCorrection = true;
301                 }
302             }
303             mMainDict.getWords(wordComposer, this, mNextLettersFrequencies);
304             if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM)
305                     && mSuggestions.size() > 0) {
306                 mHaveCorrection = true;
307             }
308         }
309         if (mOriginalWord != null) {
310             mSuggestions.add(0, mOriginalWord.toString());
311         }
312 
313         // Check if the first suggestion has a minimum number of characters in common
314         if (wordComposer.size() > 1 && mSuggestions.size() > 1
315                 && (mCorrectionMode == CORRECTION_FULL
316                 || mCorrectionMode == CORRECTION_FULL_BIGRAM)) {
317             if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) {
318                 mHaveCorrection = false;
319             }
320         }
321         if (mAutoTextEnabled) {
322             int i = 0;
323             int max = 6;
324             // Don't autotext the suggestions from the dictionaries
325             if (mCorrectionMode == CORRECTION_BASIC) max = 1;
326             while (i < mSuggestions.size() && i < max) {
327                 String suggestedWord = mSuggestions.get(i).toString().toLowerCase();
328                 CharSequence autoText =
329                         AutoText.get(suggestedWord, 0, suggestedWord.length(), view);
330                 // Is there an AutoText correction?
331                 boolean canAdd = autoText != null;
332                 // Is that correction already the current prediction (or original word)?
333                 canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i));
334                 // Is that correction already the next predicted word?
335                 if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) {
336                     canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1));
337                 }
338                 if (canAdd) {
339                     mHaveCorrection = true;
340                     mSuggestions.add(i + 1, autoText);
341                     i++;
342                 }
343                 i++;
344             }
345         }
346         removeDupes();
347         return mSuggestions;
348     }
349 
getNextLettersFrequencies()350     public int[] getNextLettersFrequencies() {
351         return mNextLettersFrequencies;
352     }
353 
removeDupes()354     private void removeDupes() {
355         final ArrayList<CharSequence> suggestions = mSuggestions;
356         if (suggestions.size() < 2) return;
357         int i = 1;
358         // Don't cache suggestions.size(), since we may be removing items
359         while (i < suggestions.size()) {
360             final CharSequence cur = suggestions.get(i);
361             // Compare each candidate with each previous candidate
362             for (int j = 0; j < i; j++) {
363                 CharSequence previous = suggestions.get(j);
364                 if (TextUtils.equals(cur, previous)) {
365                     removeFromSuggestions(i);
366                     i--;
367                     break;
368                 }
369             }
370             i++;
371         }
372     }
373 
removeFromSuggestions(int index)374     private void removeFromSuggestions(int index) {
375         CharSequence garbage = mSuggestions.remove(index);
376         if (garbage != null && garbage instanceof StringBuilder) {
377             mStringPool.add(garbage);
378         }
379     }
380 
hasMinimalCorrection()381     public boolean hasMinimalCorrection() {
382         return mHaveCorrection;
383     }
384 
compareCaseInsensitive(final String mLowerOriginalWord, final char[] word, final int offset, final int length)385     private boolean compareCaseInsensitive(final String mLowerOriginalWord,
386             final char[] word, final int offset, final int length) {
387         final int originalLength = mLowerOriginalWord.length();
388         if (originalLength == length && Character.isUpperCase(word[offset])) {
389             for (int i = 0; i < originalLength; i++) {
390                 if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) {
391                     return false;
392                 }
393             }
394             return true;
395         }
396         return false;
397     }
398 
addWord(final char[] word, final int offset, final int length, int freq, final int dicTypeId, final Dictionary.DataType dataType)399     public boolean addWord(final char[] word, final int offset, final int length, int freq,
400             final int dicTypeId, final Dictionary.DataType dataType) {
401         Dictionary.DataType dataTypeForLog = dataType;
402         ArrayList<CharSequence> suggestions;
403         int[] priorities;
404         int prefMaxSuggestions;
405         if(dataType == Dictionary.DataType.BIGRAM) {
406             suggestions = mBigramSuggestions;
407             priorities = mBigramPriorities;
408             prefMaxSuggestions = PREF_MAX_BIGRAMS;
409         } else {
410             suggestions = mSuggestions;
411             priorities = mPriorities;
412             prefMaxSuggestions = mPrefMaxSuggestions;
413         }
414 
415         int pos = 0;
416 
417         // Check if it's the same word, only caps are different
418         if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) {
419             pos = 0;
420         } else {
421             if (dataType == Dictionary.DataType.UNIGRAM) {
422                 // Check if the word was already added before (by bigram data)
423                 int bigramSuggestion = searchBigramSuggestion(word,offset,length);
424                 if(bigramSuggestion >= 0) {
425                     dataTypeForLog = Dictionary.DataType.BIGRAM;
426                     // turn freq from bigram into multiplier specified above
427                     double multiplier = (((double) mBigramPriorities[bigramSuggestion])
428                             / MAXIMUM_BIGRAM_FREQUENCY)
429                             * (BIGRAM_MULTIPLIER_MAX - BIGRAM_MULTIPLIER_MIN)
430                             + BIGRAM_MULTIPLIER_MIN;
431                     /* Log.d(TAG,"bigram num: " + bigramSuggestion
432                             + "  wordB: " + mBigramSuggestions.get(bigramSuggestion).toString()
433                             + "  currentPriority: " + freq + "  bigramPriority: "
434                             + mBigramPriorities[bigramSuggestion]
435                             + "  multiplier: " + multiplier); */
436                     freq = (int)Math.round((freq * multiplier));
437                 }
438             }
439 
440             // Check the last one's priority and bail
441             if (priorities[prefMaxSuggestions - 1] >= freq) return true;
442             while (pos < prefMaxSuggestions) {
443                 if (priorities[pos] < freq
444                         || (priorities[pos] == freq && length < suggestions.get(pos).length())) {
445                     break;
446                 }
447                 pos++;
448             }
449         }
450         if (pos >= prefMaxSuggestions) {
451             return true;
452         }
453 
454         System.arraycopy(priorities, pos, priorities, pos + 1,
455                 prefMaxSuggestions - pos - 1);
456         priorities[pos] = freq;
457         int poolSize = mStringPool.size();
458         StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
459                 : new StringBuilder(getApproxMaxWordLength());
460         sb.setLength(0);
461         // TODO: Must pay attention to locale when changing case.
462         if (mIsAllUpperCase) {
463             sb.append(new String(word, offset, length).toUpperCase());
464         } else if (mIsFirstCharCapitalized) {
465             sb.append(Character.toUpperCase(word[offset]));
466             if (length > 1) {
467                 sb.append(word, offset + 1, length - 1);
468             }
469         } else {
470             sb.append(word, offset, length);
471         }
472         suggestions.add(pos, sb);
473         if (suggestions.size() > prefMaxSuggestions) {
474             CharSequence garbage = suggestions.remove(prefMaxSuggestions);
475             if (garbage instanceof StringBuilder) {
476                 mStringPool.add(garbage);
477             }
478         } else {
479             LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
480         }
481         return true;
482     }
483 
searchBigramSuggestion(final char[] word, final int offset, final int length)484     private int searchBigramSuggestion(final char[] word, final int offset, final int length) {
485         // TODO This is almost O(n^2). Might need fix.
486         // search whether the word appeared in bigram data
487         int bigramSuggestSize = mBigramSuggestions.size();
488         for(int i = 0; i < bigramSuggestSize; i++) {
489             if(mBigramSuggestions.get(i).length() == length) {
490                 boolean chk = true;
491                 for(int j = 0; j < length; j++) {
492                     if(mBigramSuggestions.get(i).charAt(j) != word[offset+j]) {
493                         chk = false;
494                         break;
495                     }
496                 }
497                 if(chk) return i;
498             }
499         }
500 
501         return -1;
502     }
503 
isValidWord(final CharSequence word)504     public boolean isValidWord(final CharSequence word) {
505         if (word == null || word.length() == 0) {
506             return false;
507         }
508         return mMainDict.isValidWord(word)
509                 || (mUserDictionary != null && mUserDictionary.isValidWord(word))
510                 || (mAutoDictionary != null && mAutoDictionary.isValidWord(word))
511                 || (mContactsDictionary != null && mContactsDictionary.isValidWord(word));
512     }
513 
collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions)514     private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) {
515         int poolSize = mStringPool.size();
516         int garbageSize = suggestions.size();
517         while (poolSize < prefMaxSuggestions && garbageSize > 0) {
518             CharSequence garbage = suggestions.get(garbageSize - 1);
519             if (garbage != null && garbage instanceof StringBuilder) {
520                 mStringPool.add(garbage);
521                 poolSize++;
522             }
523             garbageSize--;
524         }
525         if (poolSize == prefMaxSuggestions + 1) {
526             Log.w("Suggest", "String pool got too big: " + poolSize);
527         }
528         suggestions.clear();
529     }
530 
close()531     public void close() {
532         if (mMainDict != null) {
533             mMainDict.close();
534         }
535     }
536 }
537