1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
20
21import android.text.TextUtils;
22import android.util.Log;
23
24import java.util.ArrayList;
25import java.util.concurrent.ConcurrentHashMap;
26
27public class AutoCorrection {
28    private static final boolean DBG = LatinImeLogger.sDBG;
29    private static final String TAG = AutoCorrection.class.getSimpleName();
30
31    private AutoCorrection() {
32        // Purely static class: can't instantiate.
33    }
34
35    public static CharSequence computeAutoCorrectionWord(
36            final ConcurrentHashMap<String, Dictionary> dictionaries,
37            final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
38            final CharSequence consideredWord, final float autoCorrectionThreshold,
39            final CharSequence whitelistedWord) {
40        if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) {
41            return whitelistedWord;
42        } else if (hasAutoCorrectionForConsideredWord(
43                dictionaries, wordComposer, suggestions, consideredWord)) {
44            return consideredWord;
45        } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions,
46                consideredWord, autoCorrectionThreshold)) {
47            return suggestions.get(0).mWord;
48        }
49        return null;
50    }
51
52    public static boolean isValidWord(final ConcurrentHashMap<String, Dictionary> dictionaries,
53            CharSequence word, boolean ignoreCase) {
54        if (TextUtils.isEmpty(word)) {
55            return false;
56        }
57        final CharSequence lowerCasedWord = word.toString().toLowerCase();
58        for (final String key : dictionaries.keySet()) {
59            if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
60            final Dictionary dictionary = dictionaries.get(key);
61            // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
62            // managing to get null in here. Presumably the language is changing to a language with
63            // no main dictionary and the monkey manages to type a whole word before the thread
64            // that reads the dictionary is started or something?
65            // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
66            // would be immutable once it's finished initializing, but concretely a null test is
67            // probably good enough for the time being.
68            if (null == dictionary) continue;
69            if (dictionary.isValidWord(word)
70                    || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
71                return true;
72            }
73        }
74        return false;
75    }
76
77    public static int getMaxFrequency(final ConcurrentHashMap<String, Dictionary> dictionaries,
78            CharSequence word) {
79        if (TextUtils.isEmpty(word)) {
80            return Dictionary.NOT_A_PROBABILITY;
81        }
82        int maxFreq = -1;
83        for (final String key : dictionaries.keySet()) {
84            if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
85            final Dictionary dictionary = dictionaries.get(key);
86            if (null == dictionary) continue;
87            final int tempFreq = dictionary.getFrequency(word);
88            if (tempFreq >= maxFreq) {
89                maxFreq = tempFreq;
90            }
91        }
92        return maxFreq;
93    }
94
95    public static boolean allowsToBeAutoCorrected(
96            final ConcurrentHashMap<String, Dictionary> dictionaries,
97            final CharSequence word, final boolean ignoreCase) {
98        final WhitelistDictionary whitelistDictionary =
99                (WhitelistDictionary)dictionaries.get(Suggest.DICT_KEY_WHITELIST);
100        // If "word" is in the whitelist dictionary, it should not be auto corrected.
101        if (whitelistDictionary != null
102                && whitelistDictionary.shouldForciblyAutoCorrectFrom(word)) {
103            return true;
104        }
105        return !isValidWord(dictionaries, word, ignoreCase);
106    }
107
108    private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) {
109        return whiteListedWord != null;
110    }
111
112    private static boolean hasAutoCorrectionForConsideredWord(
113            final ConcurrentHashMap<String, Dictionary> dictionaries,
114            final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
115            final CharSequence consideredWord) {
116        if (TextUtils.isEmpty(consideredWord)) return false;
117        return wordComposer.size() > 1 && suggestions.size() > 0
118                && !allowsToBeAutoCorrected(dictionaries, consideredWord, false);
119    }
120
121    private static boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer,
122            ArrayList<SuggestedWordInfo> suggestions,
123            CharSequence consideredWord, float autoCorrectionThreshold) {
124        if (wordComposer.size() > 1 && suggestions.size() > 0) {
125            final SuggestedWordInfo autoCorrectionSuggestion = suggestions.get(0);
126            //final int autoCorrectionSuggestionScore = sortedScores[0];
127            final int autoCorrectionSuggestionScore = autoCorrectionSuggestion.mScore;
128            // TODO: when the normalized score of the first suggestion is nearly equals to
129            //       the normalized score of the second suggestion, behave less aggressive.
130            final float normalizedScore = BinaryDictionary.calcNormalizedScore(
131                    consideredWord.toString(), autoCorrectionSuggestion.mWord.toString(),
132                    autoCorrectionSuggestionScore);
133            if (DBG) {
134                Log.d(TAG, "Normalized " + consideredWord + "," + autoCorrectionSuggestion + ","
135                        + autoCorrectionSuggestionScore + ", " + normalizedScore
136                        + "(" + autoCorrectionThreshold + ")");
137            }
138            if (normalizedScore >= autoCorrectionThreshold) {
139                if (DBG) {
140                    Log.d(TAG, "Auto corrected by S-threshold.");
141                }
142                return true;
143            }
144        }
145        return false;
146    }
147
148}
149