aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalSubtype.java2
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java4
-rw-r--r--java/src/com/android/inputmethod/latin/AutoCorrection.java94
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java204
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java4
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java69
-rw-r--r--java/src/com/android/inputmethod/latin/BoundedTreeSet.java49
-rw-r--r--java/src/com/android/inputmethod/latin/CollectionUtils.java95
-rw-r--r--java/src/com/android/inputmethod/latin/Constants.java14
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java22
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsDictionary.java178
-rw-r--r--java/src/com/android/inputmethod/latin/DicTraverseSession.java75
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java80
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java51
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java13
-rw-r--r--java/src/com/android/inputmethod/latin/EditingUtils.java182
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java55
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java147
-rw-r--r--java/src/com/android/inputmethod/latin/ImfUtils.java7
-rw-r--r--java/src/com/android/inputmethod/latin/InputAttributes.java11
-rw-r--r--java/src/com/android/inputmethod/latin/InputPointers.java133
-rw-r--r--java/src/com/android/inputmethod/latin/InputView.java13
-rw-r--r--java/src/com/android/inputmethod/latin/JniUtils.java6
-rw-r--r--java/src/com/android/inputmethod/latin/LastComposedWord.java18
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java1534
-rw-r--r--java/src/com/android/inputmethod/latin/LatinImeLogger.java2
-rw-r--r--java/src/com/android/inputmethod/latin/LocaleUtils.java2
-rw-r--r--java/src/com/android/inputmethod/latin/ResearchLogger.java757
-rw-r--r--java/src/com/android/inputmethod/latin/ResizableIntArray.java146
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java455
-rw-r--r--java/src/com/android/inputmethod/latin/Settings.java137
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsValues.java106
-rw-r--r--java/src/com/android/inputmethod/latin/StringUtils.java5
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeLocale.java10
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java96
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java582
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java71
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java11
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java54
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java10
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java56
-rw-r--r--java/src/com/android/inputmethod/latin/UserBinaryDictionary.java47
-rw-r--r--java/src/com/android/inputmethod/latin/UserDictionary.java225
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictionary.java41
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java17
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java16
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java56
-rw-r--r--java/src/com/android/inputmethod/latin/WhitelistDictionary.java109
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java168
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java266
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java16
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Word.java8
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java475
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java154
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java25
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java303
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java59
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java10
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java10
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java (renamed from java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java)107
61 files changed, 3545 insertions, 4129 deletions
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
index f8f1395b3..4b47a261f 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
@@ -91,7 +91,7 @@ public class AdditionalSubtype {
}
final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR);
final ArrayList<InputMethodSubtype> subtypesList =
- new ArrayList<InputMethodSubtype>(prefSubtypeArray.length);
+ CollectionUtils.newArrayList(prefSubtypeArray.length);
for (final String prefSubtype : prefSubtypeArray) {
final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype);
if (subtype.getNameResId() == SubtypeLocale.UNKNOWN_KEYBOARD_LAYOUT) {
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
index 779a38823..d01592a4d 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
@@ -89,7 +89,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
super(context, android.R.layout.simple_spinner_item);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- final TreeSet<SubtypeLocaleItem> items = new TreeSet<SubtypeLocaleItem>();
+ final TreeSet<SubtypeLocaleItem> items = CollectionUtils.newTreeSet();
final InputMethodInfo imi = ImfUtils.getInputMethodInfoOfThisIme(context);
final int count = imi.getSubtypeCount();
for (int i = 0; i < count; i++) {
@@ -533,7 +533,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
private InputMethodSubtype[] getSubtypes() {
final PreferenceGroup group = getPreferenceScreen();
- final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+ final ArrayList<InputMethodSubtype> subtypes = CollectionUtils.newArrayList();
final int count = group.getPreferenceCount();
for (int i = 0; i < count; i++) {
final Preference pref = group.getPreference(i);
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index e0452483c..01ba30077 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -21,34 +21,17 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import android.text.TextUtils;
import android.util.Log;
-import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
public class AutoCorrection {
private static final boolean DBG = LatinImeLogger.sDBG;
private static final String TAG = AutoCorrection.class.getSimpleName();
+ private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
private AutoCorrection() {
// Purely static class: can't instantiate.
}
- public static CharSequence computeAutoCorrectionWord(
- final ConcurrentHashMap<String, Dictionary> dictionaries,
- final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
- final CharSequence consideredWord, final float autoCorrectionThreshold,
- final CharSequence whitelistedWord) {
- if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) {
- return whitelistedWord;
- } else if (hasAutoCorrectionForConsideredWord(
- dictionaries, wordComposer, suggestions, consideredWord)) {
- return consideredWord;
- } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions,
- consideredWord, autoCorrectionThreshold)) {
- return suggestions.get(0).mWord;
- }
- return null;
- }
-
public static boolean isValidWord(final ConcurrentHashMap<String, Dictionary> dictionaries,
CharSequence word, boolean ignoreCase) {
if (TextUtils.isEmpty(word)) {
@@ -56,7 +39,6 @@ public class AutoCorrection {
}
final CharSequence lowerCasedWord = word.toString().toLowerCase();
for (final String key : dictionaries.keySet()) {
- if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
final Dictionary dictionary = dictionaries.get(key);
// It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
// managing to get null in here. Presumably the language is changing to a language with
@@ -81,7 +63,6 @@ public class AutoCorrection {
}
int maxFreq = -1;
for (final String key : dictionaries.keySet()) {
- if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
final Dictionary dictionary = dictionaries.get(key);
if (null == dictionary) continue;
final int tempFreq = dictionary.getFrequency(word);
@@ -92,46 +73,26 @@ public class AutoCorrection {
return maxFreq;
}
- public static boolean allowsToBeAutoCorrected(
+ // Returns true if this isn't in any dictionary.
+ public static boolean isNotAWord(
final ConcurrentHashMap<String, Dictionary> dictionaries,
final CharSequence word, final boolean ignoreCase) {
- final WhitelistDictionary whitelistDictionary =
- (WhitelistDictionary)dictionaries.get(Suggest.DICT_KEY_WHITELIST);
- // If "word" is in the whitelist dictionary, it should not be auto corrected.
- if (whitelistDictionary != null
- && whitelistDictionary.shouldForciblyAutoCorrectFrom(word)) {
- return true;
- }
return !isValidWord(dictionaries, word, ignoreCase);
}
- private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) {
- return whiteListedWord != null;
- }
-
- private static boolean hasAutoCorrectionForConsideredWord(
- final ConcurrentHashMap<String, Dictionary> dictionaries,
- final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
- final CharSequence consideredWord) {
- if (TextUtils.isEmpty(consideredWord)) return false;
- return wordComposer.size() > 1 && suggestions.size() > 0
- && !allowsToBeAutoCorrected(dictionaries, consideredWord, false);
- }
-
- private static boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer,
- ArrayList<SuggestedWordInfo> suggestions,
+ public static boolean suggestionExceedsAutoCorrectionThreshold(SuggestedWordInfo suggestion,
CharSequence consideredWord, float autoCorrectionThreshold) {
- if (wordComposer.size() > 1 && suggestions.size() > 0) {
- final SuggestedWordInfo autoCorrectionSuggestion = suggestions.get(0);
- //final int autoCorrectionSuggestionScore = sortedScores[0];
- final int autoCorrectionSuggestionScore = autoCorrectionSuggestion.mScore;
+ if (null != suggestion) {
+ // Shortlist a whitelisted word
+ if (suggestion.mKind == SuggestedWordInfo.KIND_WHITELIST) return true;
+ final int autoCorrectionSuggestionScore = suggestion.mScore;
// TODO: when the normalized score of the first suggestion is nearly equals to
// the normalized score of the second suggestion, behave less aggressive.
final float normalizedScore = BinaryDictionary.calcNormalizedScore(
- consideredWord.toString(), autoCorrectionSuggestion.mWord.toString(),
+ consideredWord.toString(), suggestion.mWord.toString(),
autoCorrectionSuggestionScore);
if (DBG) {
- Log.d(TAG, "Normalized " + consideredWord + "," + autoCorrectionSuggestion + ","
+ Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
+ autoCorrectionSuggestionScore + ", " + normalizedScore
+ "(" + autoCorrectionThreshold + ")");
}
@@ -139,10 +100,43 @@ public class AutoCorrection {
if (DBG) {
Log.d(TAG, "Auto corrected by S-threshold.");
}
- return true;
+ return !shouldBlockAutoCorrectionBySafetyNet(consideredWord.toString(),
+ suggestion.mWord);
}
}
return false;
}
+ // TODO: Resolve the inconsistencies between the native auto correction algorithms and
+ // this safety net
+ public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
+ final CharSequence suggestion) {
+ // Safety net for auto correction.
+ // Actually if we hit this safety net, it's a bug.
+ // If user selected aggressive auto correction mode, there is no need to use the safety
+ // net.
+ // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
+ // we should not use net because relatively edit distance can be big.
+ final int typedWordLength = typedWord.length();
+ if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) {
+ return false;
+ }
+ final int maxEditDistanceOfNativeDictionary =
+ (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
+ final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
+ if (DBG) {
+ Log.d(TAG, "Autocorrected edit distance = " + distance
+ + ", " + maxEditDistanceOfNativeDictionary);
+ }
+ if (distance > maxEditDistanceOfNativeDictionary) {
+ if (DBG) {
+ Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
+ Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
+ + "Turning off auto-correction.");
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index d0613bd72..8909526d8 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -18,9 +18,12 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.text.TextUtils;
+import android.util.SparseArray;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
@@ -40,22 +43,44 @@ public class BinaryDictionary extends Dictionary {
*/
public static final int MAX_WORD_LENGTH = 48;
public static final int MAX_WORDS = 18;
+ public static final int MAX_SPACES = 16;
private static final String TAG = "BinaryDictionary";
- private static final int MAX_BIGRAMS = 60;
+ private static final int MAX_PREDICTIONS = 60;
+ private static final int MAX_RESULTS = Math.max(MAX_PREDICTIONS, MAX_WORDS);
private static final int TYPED_LETTER_MULTIPLIER = 2;
- private int mDicTypeId;
private long mNativeDict;
- private final int[] mInputCodes = new int[MAX_WORD_LENGTH];
- private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
- private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
- private final int[] mScores = new int[MAX_WORDS];
- private final int[] mBigramScores = new int[MAX_BIGRAMS];
+ private final Locale mLocale;
+ private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH];
+ // TODO: The below should be int[] mOutputCodePoints
+ private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_RESULTS];
+ private final int[] mSpaceIndices = new int[MAX_SPACES];
+ private final int[] mOutputScores = new int[MAX_RESULTS];
+ private final int[] mOutputTypes = new int[MAX_RESULTS];
private final boolean mUseFullEditDistance;
+ private final SparseArray<DicTraverseSession> mDicTraverseSessions =
+ CollectionUtils.newSparseArray();
+
+ // TODO: There should be a way to remove used DicTraverseSession objects from
+ // {@code mDicTraverseSessions}.
+ private DicTraverseSession getTraverseSession(int traverseSessionId) {
+ synchronized(mDicTraverseSessions) {
+ DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId);
+ if (traverseSession == null) {
+ traverseSession = mDicTraverseSessions.get(traverseSessionId);
+ if (traverseSession == null) {
+ traverseSession = new DicTraverseSession(mLocale, mNativeDict);
+ mDicTraverseSessions.put(traverseSessionId, traverseSession);
+ }
+ }
+ return traverseSession;
+ }
+ }
+
/**
* Constructor for the binary dictionary. This is supposed to be called from the
* dictionary factory.
@@ -65,14 +90,13 @@ public class BinaryDictionary extends Dictionary {
* @param offset the offset of the dictionary data within the file.
* @param length the length of the binary data.
* @param useFullEditDistance whether to use the full edit distance in suggestions
+ * @param dictType the dictionary type, as a human-readable string
*/
public BinaryDictionary(final Context context,
final String filename, final long offset, final long length,
- final boolean useFullEditDistance, final Locale locale) {
- // Note: at the moment a binary dictionary is always of the "main" type.
- // Initializing this here will help transitioning out of the scheme where
- // the Suggest class knows everything about every single dictionary.
- mDicTypeId = Suggest.DIC_MAIN;
+ final boolean useFullEditDistance, final Locale locale, final String dictType) {
+ super(dictType);
+ mLocale = locale;
mUseFullEditDistance = useFullEditDistance;
loadDictionary(filename, offset, length);
}
@@ -82,121 +106,88 @@ public class BinaryDictionary extends Dictionary {
}
private native long openNative(String sourceDir, long dictOffset, long dictSize,
- int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords);
+ int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords,
+ int maxPredictions);
private native void closeNative(long dict);
- private native int getFrequencyNative(long dict, int[] word, int wordLength);
+ private native int getFrequencyNative(long dict, int[] word);
private native boolean isValidBigramNative(long dict, int[] word1, int[] word2);
- private native int getSuggestionsNative(long dict, long proximityInfo, int[] xCoordinates,
- int[] yCoordinates, int[] inputCodes, int codesSize, int[] prevWordForBigrams,
- boolean useFullEditDistance, char[] outputChars, int[] scores);
- private native int getBigramsNative(long dict, int[] prevWord, int prevWordLength,
- int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores,
- int maxWordLength, int maxBigrams);
- private static native float calcNormalizedScoreNative(
- char[] before, int beforeLength, char[] after, int afterLength, int score);
- private static native int editDistanceNative(
- char[] before, int beforeLength, char[] after, int afterLength);
-
+ private native int getSuggestionsNative(long dict, long proximityInfo, long traverseSession,
+ int[] xCoordinates, int[] yCoordinates, int[] times, int[] pointerIds,
+ int[] inputCodePoints, int codesSize, int commitPoint, boolean isGesture,
+ int[] prevWordCodePointArray, boolean useFullEditDistance, char[] outputChars,
+ int[] outputScores, int[] outputIndices, int[] outputTypes);
+ private static native float calcNormalizedScoreNative(char[] before, char[] after, int score);
+ private static native int editDistanceNative(char[] before, char[] after);
+
+ // TODO: Move native dict into session
private final void loadDictionary(String path, long startOffset, long length) {
- mNativeDict = openNative(path, startOffset, length,
- TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS);
+ mNativeDict = openNative(path, startOffset, length, TYPED_LETTER_MULTIPLIER,
+ FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS, MAX_PREDICTIONS);
}
@Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- if (mNativeDict == 0) return;
-
- int[] codePoints = StringUtils.toCodePointArray(previousWord.toString());
- Arrays.fill(mOutputChars_bigrams, (char) 0);
- Arrays.fill(mBigramScores, 0);
-
- int codesSize = codes.size();
- Arrays.fill(mInputCodes, -1);
- if (codesSize > 0) {
- mInputCodes[0] = codes.getCodeAt(0);
- }
-
- int count = getBigramsNative(mNativeDict, codePoints, codePoints.length, mInputCodes,
- codesSize, mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS);
- if (count > MAX_BIGRAMS) {
- count = MAX_BIGRAMS;
- }
-
- for (int j = 0; j < count; ++j) {
- if (codesSize > 0 && mBigramScores[j] < 1) break;
- final int start = j * MAX_WORD_LENGTH;
- int len = 0;
- while (len < MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) {
- ++len;
- }
- if (len > 0) {
- callback.addWord(mOutputChars_bigrams, start, len, mBigramScores[j],
- mDicTypeId, Dictionary.BIGRAM);
- }
- }
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, 0);
}
- // proximityInfo and/or prevWordForBigrams may not be null.
@Override
- public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
- final int count = getSuggestions(codes, prevWordForBigrams, proximityInfo, mOutputChars,
- mScores);
+ public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) {
+ if (!isValidDictionary()) return null;
+
+ Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
+ // TODO: toLowerCase in the native code
+ final int[] prevWordCodePointArray = (null == prevWord)
+ ? null : StringUtils.toCodePointArray(prevWord.toString());
+ final int composerSize = composer.size();
+
+ final boolean isGesture = composer.isBatchMode();
+ if (composerSize <= 1 || !isGesture) {
+ if (composerSize > MAX_WORD_LENGTH - 1) return null;
+ for (int i = 0; i < composerSize; i++) {
+ mInputCodePoints[i] = composer.getCodeAt(i);
+ }
+ }
+ final InputPointers ips = composer.getInputPointers();
+ final int codesSize = isGesture ? ips.getPointerSize() : composerSize;
+ // proximityInfo and/or prevWordForBigrams may not be null.
+ final int tmpCount = getSuggestionsNative(mNativeDict,
+ proximityInfo.getNativeProximityInfo(), getTraverseSession(sessionId).getSession(),
+ ips.getXCoordinates(), ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(),
+ mInputCodePoints, codesSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray,
+ mUseFullEditDistance, mOutputChars, mOutputScores, mSpaceIndices, mOutputTypes);
+ final int count = Math.min(tmpCount, MAX_PREDICTIONS);
+
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
for (int j = 0; j < count; ++j) {
- if (mScores[j] < 1) break;
+ if (composerSize > 0 && mOutputScores[j] < 1) break;
final int start = j * MAX_WORD_LENGTH;
int len = 0;
while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) {
++len;
}
if (len > 0) {
- callback.addWord(mOutputChars, start, len, mScores[j], mDicTypeId,
- Dictionary.UNIGRAM);
+ final int score = SuggestedWordInfo.KIND_WHITELIST == mOutputTypes[j]
+ ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j];
+ suggestions.add(new SuggestedWordInfo(
+ new String(mOutputChars, start, len), score, mOutputTypes[j], mDictType));
}
}
+ return suggestions;
}
/* package for test */ boolean isValidDictionary() {
return mNativeDict != 0;
}
- // proximityInfo may not be null.
- /* package for test */ int getSuggestions(final WordComposer codes,
- final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo,
- char[] outputChars, int[] scores) {
- if (!isValidDictionary()) return -1;
-
- final int codesSize = codes.size();
- // Won't deal with really long words.
- if (codesSize > MAX_WORD_LENGTH - 1) return -1;
-
- Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE);
- for (int i = 0; i < codesSize; i++) {
- mInputCodes[i] = codes.getCodeAt(i);
- }
- Arrays.fill(outputChars, (char) 0);
- Arrays.fill(scores, 0);
-
- final int[] prevWordCodePointArray = null == prevWordForBigrams
- ? null : StringUtils.toCodePointArray(prevWordForBigrams.toString());
-
- // TODO: pass the previous word to native code
- return getSuggestionsNative(
- mNativeDict, proximityInfo.getNativeProximityInfo(),
- codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize,
- prevWordCodePointArray, mUseFullEditDistance, outputChars, scores);
- }
-
public static float calcNormalizedScore(String before, String after, int score) {
- return calcNormalizedScoreNative(before.toCharArray(), before.length(),
- after.toCharArray(), after.length(), score);
+ return calcNormalizedScoreNative(before.toCharArray(), after.toCharArray(), score);
}
public static int editDistance(String before, String after) {
- return editDistanceNative(
- before.toCharArray(), before.length(), after.toCharArray(), after.length());
+ return editDistanceNative(before.toCharArray(), after.toCharArray());
}
@Override
@@ -207,8 +198,8 @@ public class BinaryDictionary extends Dictionary {
@Override
public int getFrequency(CharSequence word) {
if (word == null) return -1;
- int[] chars = StringUtils.toCodePointArray(word.toString());
- return getFrequencyNative(mNativeDict, chars, chars.length);
+ int[] codePoints = StringUtils.toCodePointArray(word.toString());
+ return getFrequencyNative(mNativeDict, codePoints);
}
// TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni
@@ -221,11 +212,20 @@ public class BinaryDictionary extends Dictionary {
}
@Override
- public synchronized void close() {
+ public void close() {
+ synchronized (mDicTraverseSessions) {
+ final int sessionsSize = mDicTraverseSessions.size();
+ for (int index = 0; index < sessionsSize; ++index) {
+ final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index);
+ if (traverseSession != null) {
+ traverseSession.close();
+ }
+ }
+ }
closeInternal();
}
- private void closeInternal() {
+ private synchronized void closeInternal() {
if (mNativeDict != 0) {
closeNative(mNativeDict);
mNativeDict = 0;
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 236c198ad..799aea8ef 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -99,7 +99,7 @@ public class BinaryDictionaryFileDumper {
}
try {
- final List<WordListInfo> list = new ArrayList<WordListInfo>();
+ final List<WordListInfo> list = CollectionUtils.newArrayList();
do {
final String wordListId = c.getString(0);
final String wordListLocale = c.getString(1);
@@ -267,7 +267,7 @@ public class BinaryDictionaryFileDumper {
final ContentResolver resolver = context.getContentResolver();
final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
hasDefaultWordList);
- final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
+ final List<AssetFileAddress> fileAddressList = CollectionUtils.newArrayList();
for (WordListInfo id : idList) {
final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context);
if (null != afd) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 063243e1b..e1cb195bc 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -23,6 +25,10 @@ import android.content.res.AssetFileDescriptor;
import android.util.Log;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
@@ -51,6 +57,9 @@ class BinaryDictionaryGetter {
private static final String MAIN_DICTIONARY_CATEGORY = "main";
public static final String ID_CATEGORY_SEPARATOR = ":";
+ // The key considered to read the version attribute in a dictionary file.
+ private static String VERSION_KEY = "version";
+
// Prevents this from being instantiated
private BinaryDictionaryGetter() {}
@@ -254,8 +263,7 @@ class BinaryDictionaryGetter {
final Context context) {
final File[] directoryList = getCachedDirectoryList(context);
if (null == directoryList) return EMPTY_FILE_ARRAY;
- final HashMap<String, FileAndMatchLevel> cacheFiles =
- new HashMap<String, FileAndMatchLevel>();
+ final HashMap<String, FileAndMatchLevel> cacheFiles = CollectionUtils.newHashMap();
for (File directory : directoryList) {
if (!directory.isDirectory()) continue;
final String dirLocale = getWordListIdFromFileName(directory.getName());
@@ -336,6 +344,54 @@ class BinaryDictionaryGetter {
return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
}
+ // ## HACK ## we prevent usage of a dictionary before version 18 for English only. The reason
+ // for this is, since those do not include whitelist entries, the new code with an old version
+ // of the dictionary would lose whitelist functionality.
+ private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) {
+ // Only for English - other languages didn't have a whitelist, hence this
+ // ad-hock ## HACK ##
+ if (!Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) return true;
+
+ FileInputStream inStream = null;
+ try {
+ // Read the version of the file
+ inStream = new FileInputStream(f);
+ final ByteBuffer buffer = inStream.getChannel().map(
+ FileChannel.MapMode.READ_ONLY, 0, f.length());
+ final int magic = buffer.getInt();
+ if (magic != BinaryDictInputOutput.VERSION_2_MAGIC_NUMBER) {
+ return false;
+ }
+ final int formatVersion = buffer.getInt();
+ final int headerSize = buffer.getInt();
+ final HashMap<String, String> options = CollectionUtils.newHashMap();
+ BinaryDictInputOutput.populateOptions(buffer, headerSize, options);
+
+ final String version = options.get(VERSION_KEY);
+ if (null == version) {
+ // No version in the options : the format is unexpected
+ return false;
+ }
+ // Version 18 is the first one to include the whitelist
+ // Obviously this is a big ## HACK ##
+ return Integer.parseInt(version) >= 18;
+ } catch (java.io.FileNotFoundException e) {
+ return false;
+ } catch (java.io.IOException e) {
+ return false;
+ } catch (NumberFormatException e) {
+ return false;
+ } finally {
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+ }
+ }
+
/**
* Returns a list of file addresses for a given locale, trying relevant methods in order.
*
@@ -362,18 +418,19 @@ class BinaryDictionaryGetter {
final DictPackSettings dictPackSettings = new DictPackSettings(context);
boolean foundMainDict = false;
- final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
+ final ArrayList<AssetFileAddress> fileList = CollectionUtils.newArrayList();
// cachedWordLists may not be null, see doc for getCachedDictionaryList
for (final File f : cachedWordLists) {
final String wordListId = getWordListIdFromFileName(f.getName());
- if (isMainWordListId(wordListId)) {
+ final boolean canUse = f.canRead() && hackCanUseDictionaryFile(locale, f);
+ if (canUse && isMainWordListId(wordListId)) {
foundMainDict = true;
}
if (!dictPackSettings.isWordListActive(wordListId)) continue;
- if (f.canRead()) {
+ if (canUse) {
fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
} else {
- Log.e(TAG, "Found a cached dictionary file but cannot read it");
+ Log.e(TAG, "Found a cached dictionary file but cannot read or use it");
}
}
diff --git a/java/src/com/android/inputmethod/latin/BoundedTreeSet.java b/java/src/com/android/inputmethod/latin/BoundedTreeSet.java
new file mode 100644
index 000000000..cf977617d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/BoundedTreeSet.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+/**
+ * A TreeSet that is bounded in size and throws everything that's smaller than its limit
+ */
+public class BoundedTreeSet extends TreeSet<SuggestedWordInfo> {
+ private final int mCapacity;
+ public BoundedTreeSet(final Comparator<SuggestedWordInfo> comparator, final int capacity) {
+ super(comparator);
+ mCapacity = capacity;
+ }
+
+ @Override
+ public boolean add(final SuggestedWordInfo e) {
+ if (size() < mCapacity) return super.add(e);
+ if (comparator().compare(e, last()) > 0) return false;
+ super.add(e);
+ pollLast(); // removes the last element
+ return true;
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends SuggestedWordInfo> e) {
+ if (null == e) return false;
+ return super.addAll(e);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/CollectionUtils.java b/java/src/com/android/inputmethod/latin/CollectionUtils.java
new file mode 100644
index 000000000..baa2ee1cd
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/CollectionUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin;
+
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class CollectionUtils {
+ private CollectionUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static <K,V> HashMap<K,V> newHashMap() {
+ return new HashMap<K,V>();
+ }
+
+ public static <K,V> TreeMap<K,V> newTreeMap() {
+ return new TreeMap<K,V>();
+ }
+
+ public static <K, V> Map<K,V> newSynchronizedTreeMap() {
+ final TreeMap<K,V> treeMap = newTreeMap();
+ return Collections.synchronizedMap(treeMap);
+ }
+
+ public static <K,V> ConcurrentHashMap<K,V> newConcurrentHashMap() {
+ return new ConcurrentHashMap<K,V>();
+ }
+
+ public static <E> HashSet<E> newHashSet() {
+ return new HashSet<E>();
+ }
+
+ public static <E> TreeSet<E> newTreeSet() {
+ return new TreeSet<E>();
+ }
+
+ public static <E> ArrayList<E> newArrayList() {
+ return new ArrayList<E>();
+ }
+
+ public static <E> ArrayList<E> newArrayList(final int initialCapacity) {
+ return new ArrayList<E>(initialCapacity);
+ }
+
+ public static <E> ArrayList<E> newArrayList(final Collection<E> collection) {
+ return new ArrayList<E>(collection);
+ }
+
+ public static <E> LinkedList<E> newLinkedList() {
+ return new LinkedList<E>();
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList() {
+ return new CopyOnWriteArrayList<E>();
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(
+ final Collection<E> collection) {
+ return new CopyOnWriteArrayList<E>(collection);
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(final E[] array) {
+ return new CopyOnWriteArrayList<E>(array);
+ }
+
+ public static <E> SparseArray<E> newSparseArray() {
+ return new SparseArray<E>();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index e79db367c..d71c0f995 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -19,6 +19,13 @@ package com.android.inputmethod.latin;
import android.view.inputmethod.EditorInfo;
public final class Constants {
+ public static final class Color {
+ /**
+ * The alpha value for fully opaque.
+ */
+ public final static int ALPHA_OPAQUE = 255;
+ }
+
public static final class ImeOption {
/**
* The private IME option used to indicate that no microphone should be shown for a given
@@ -121,6 +128,13 @@ public final class Constants {
}
}
+ public static final int NOT_A_CODE = -1;
+
+ // See {@link KeyboardActionListener.Adapter#isInvalidCoordinate(int)}.
+ public static final int NOT_A_COORDINATE = -1;
+ public static final int SUGGESTION_STRIP_COORDINATE = -2;
+ public static final int SPELL_CHECKER_COORDINATE = -3;
+
private Constants() {
// This utility class is not publicly instantiable.
}
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 34308dfb3..5edc4314f 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -52,6 +52,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
/** The number of contacts in the most recent dictionary rebuild. */
static private int sContactCountAtLastRebuild = 0;
+ /** The locale for this contacts dictionary. Controls name bigram predictions. */
+ public final Locale mLocale;
+
private ContentObserver mObserver;
/**
@@ -59,8 +62,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
*/
private final boolean mUseFirstLastBigrams;
- public ContactsBinaryDictionary(final Context context, final int dicTypeId, Locale locale) {
- super(context, getFilenameWithLocale(NAME, locale.toString()), dicTypeId);
+ public ContactsBinaryDictionary(final Context context, Locale locale) {
+ super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS);
+ mLocale = locale;
mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
registerObserver(context);
@@ -116,12 +120,6 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
}
}
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- super.getBigrams(codes, previousWord, callback);
- }
-
private boolean useFirstLastBigramsForLocale(Locale locale) {
// TODO: Add firstname/lastname bigram rules for other languages.
if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
@@ -163,7 +161,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
* bigrams depending on locale.
*/
private void addName(String name) {
- int len = name.codePointCount(0, name.length());
+ int len = StringUtils.codePointCount(name);
String prevWord = null;
// TODO: Better tokenization for non-Latin writing systems
for (int i = 0; i < len; i++) {
@@ -173,7 +171,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
i = end - 1;
// Don't add single letter words, possibly confuses
// capitalization of i.
- final int wordLen = word.codePointCount(0, word.length());
+ final int wordLen = StringUtils.codePointCount(word);
if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS);
if (!TextUtils.isEmpty(prevWord)) {
@@ -262,14 +260,14 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
* Checks if the words in a name are in the current binary dictionary.
*/
private boolean isNameInDictionary(String name) {
- int len = name.codePointCount(0, name.length());
+ int len = StringUtils.codePointCount(name);
String prevWord = null;
for (int i = 0; i < len; i++) {
if (Character.isLetter(name.codePointAt(i))) {
int end = getWordEndPosition(name, len, i);
String word = name.substring(i, end);
i = end - 1;
- final int wordLen = word.codePointCount(0, word.length());
+ final int wordLen = StringUtils.codePointCount(word);
if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
if (!super.isValidBigramLocked(prevWord, word)) {
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
deleted file mode 100644
index cbfbd0ec8..000000000
--- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * 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.android.inputmethod.latin;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.keyboard.Keyboard;
-
-// TODO: This class is superseded by {@link ContactsBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words from Contacts provider.
- *
- * @deprecated Use {@link ContactsBinaryDictionary}.
- */
-@Deprecated
-public class ContactsDictionary extends ExpandableDictionary {
-
- private static final String[] PROJECTION = {
- BaseColumns._ID,
- Contacts.DISPLAY_NAME,
- };
-
- private static final String TAG = "ContactsDictionary";
-
- /**
- * Frequency for contacts information into the dictionary
- */
- private static final int FREQUENCY_FOR_CONTACTS = 40;
- private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
-
- private static final int INDEX_NAME = 1;
-
- private ContentObserver mObserver;
-
- private long mLastLoadedContacts;
-
- public ContactsDictionary(final Context context, final int dicTypeId) {
- super(context, dicTypeId);
- registerObserver(context);
- loadDictionary();
- }
-
- private synchronized void registerObserver(final Context context) {
- // Perform a managed query. The Activity will handle closing and requerying the cursor
- // when needed.
- if (mObserver != null) return;
- ContentResolver cres = context.getContentResolver();
- cres.registerContentObserver(
- Contacts.CONTENT_URI, true, mObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean self) {
- setRequiresReload(true);
- }
- });
- }
-
- public void reopen(final Context context) {
- registerObserver(context);
- }
-
- @Override
- public synchronized void close() {
- if (mObserver != null) {
- getContext().getContentResolver().unregisterContentObserver(mObserver);
- mObserver = null;
- }
- super.close();
- }
-
- @Override
- public void startDictionaryLoadingTaskLocked() {
- long now = SystemClock.uptimeMillis();
- if (mLastLoadedContacts == 0
- || now - mLastLoadedContacts > 30 * 60 * 1000 /* 30 minutes */) {
- super.startDictionaryLoadingTaskLocked();
- }
- }
-
- @Override
- public void loadDictionaryAsync() {
- try {
- Cursor cursor = getContext().getContentResolver()
- .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
- if (cursor != null) {
- addWords(cursor);
- }
- } catch(IllegalStateException e) {
- Log.e(TAG, "Contacts DB is having problems");
- }
- mLastLoadedContacts = SystemClock.uptimeMillis();
- }
-
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- // Do not return bigrams from Contacts when nothing was typed.
- if (codes.size() <= 0) return;
- super.getBigrams(codes, previousWord, callback);
- }
-
- private void addWords(Cursor cursor) {
- clearDictionary();
-
- final int maxWordLength = getMaxWordLength();
- try {
- if (cursor.moveToFirst()) {
- while (!cursor.isAfterLast()) {
- String name = cursor.getString(INDEX_NAME);
-
- if (name != null && -1 == name.indexOf('@')) {
- int len = name.length();
- String prevWord = null;
-
- // TODO: Better tokenization for non-Latin writing systems
- for (int i = 0; i < len; i++) {
- if (Character.isLetter(name.charAt(i))) {
- int j;
- for (j = i + 1; j < len; j++) {
- char c = name.charAt(j);
-
- if (!(c == Keyboard.CODE_DASH
- || c == Keyboard.CODE_SINGLE_QUOTE
- || Character.isLetter(c))) {
- break;
- }
- }
-
- String word = name.substring(i, j);
- i = j - 1;
-
- // Safeguard against adding really long words. Stack
- // may overflow due to recursion
- // Also don't add single letter words, possibly confuses
- // capitalization of i.
- final int wordLen = word.length();
- if (wordLen < maxWordLength && wordLen > 1) {
- super.addWord(word, null /* shortcut */,
- FREQUENCY_FOR_CONTACTS);
- if (!TextUtils.isEmpty(prevWord)) {
- super.setBigramAndGetFrequency(prevWord, word,
- FREQUENCY_FOR_CONTACTS_BIGRAM);
- }
- prevWord = word;
- }
- }
- }
- }
- cursor.moveToNext();
- }
- }
- cursor.close();
- } catch(IllegalStateException e) {
- Log.e(TAG, "Contacts DB is having problems");
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
new file mode 100644
index 000000000..359da72cc
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012, The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin;
+
+import java.util.Locale;
+
+public class DicTraverseSession {
+ static {
+ JniUtils.loadNativeLibrary();
+ }
+
+ private native long setDicTraverseSessionNative(String locale);
+ private native void initDicTraverseSessionNative(long nativeDicTraverseSession,
+ long dictionary, int[] previousWord, int previousWordLength);
+ private native void releaseDicTraverseSessionNative(long nativeDicTraverseSession);
+
+ private long mNativeDicTraverseSession;
+
+ public DicTraverseSession(Locale locale, long dictionary) {
+ mNativeDicTraverseSession = createNativeDicTraverseSession(
+ locale != null ? locale.toString() : "");
+ initSession(dictionary);
+ }
+
+ public long getSession() {
+ return mNativeDicTraverseSession;
+ }
+
+ public void initSession(long dictionary) {
+ initSession(dictionary, null, 0);
+ }
+
+ public void initSession(long dictionary, int[] previousWord, int previousWordLength) {
+ initDicTraverseSessionNative(
+ mNativeDicTraverseSession, dictionary, previousWord, previousWordLength);
+ }
+
+ private final long createNativeDicTraverseSession(String locale) {
+ return setDicTraverseSessionNative(locale);
+ }
+
+ private void closeInternal() {
+ if (mNativeDicTraverseSession != 0) {
+ releaseDicTraverseSessionNative(mNativeDicTraverseSession);
+ mNativeDicTraverseSession = 0;
+ }
+ }
+
+ public void close() {
+ closeInternal();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ closeInternal();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 7cd9bc2a8..88d0c09dd 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -17,6 +17,9 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.ArrayList;
/**
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
@@ -28,54 +31,41 @@ public abstract class Dictionary {
*/
protected static final int FULL_WORD_SCORE_MULTIPLIER = 2;
- public static final int UNIGRAM = 0;
- public static final int BIGRAM = 1;
-
public static final int NOT_A_PROBABILITY = -1;
- /**
- * Interface to be implemented by classes requesting words to be fetched from the dictionary.
- * @see #getWords(WordComposer, CharSequence, WordCallback, ProximityInfo)
- */
- public interface WordCallback {
- /**
- * Adds a word to a list of suggestions. The word is expected to be ordered based on
- * the provided score.
- * @param word the character array containing the word
- * @param wordOffset starting offset of the word in the character array
- * @param wordLength length of valid characters in the character array
- * @param score the score of occurrence. This is normalized between 1 and 255, but
- * can exceed those limits
- * @param dicTypeId of the dictionary where word was from
- * @param dataType tells type of this data, either UNIGRAM or BIGRAM
- * @return true if the word was added, false if no more words are required
- */
- boolean addWord(char[] word, int wordOffset, int wordLength, int score, int dicTypeId,
- int dataType);
+
+ public static final String TYPE_USER_TYPED = "user_typed";
+ public static final String TYPE_APPLICATION_DEFINED = "application_defined";
+ public static final String TYPE_HARDCODED = "hardcoded"; // punctuation signs and such
+ public static final String TYPE_MAIN = "main";
+ public static final String TYPE_CONTACTS = "contacts";
+ // User dictionary, the system-managed one.
+ public static final String TYPE_USER = "user";
+ // User history dictionary internal to LatinIME.
+ public static final String TYPE_USER_HISTORY = "history";
+ protected final String mDictType;
+
+ public Dictionary(final String dictType) {
+ mDictType = dictType;
}
/**
- * Searches for words in the dictionary that match the characters in the composer. Matched
- * words are added through the callback object.
- * @param composer the key sequence to match
- * @param prevWordForBigrams the previous word, or null if none
- * @param callback the callback object to send matched words to as possible candidates
+ * Searches for suggestions for a given context. For the moment the context is only the
+ * previous word.
+ * @param composer the key sequence to match with coordinate info, as a WordComposer
+ * @param prevWord the previous word, or null if none
* @param proximityInfo the object for key proximity. May be ignored by some implementations.
- * @see WordCallback#addWord(char[], int, int, int, int, int)
+ * @return the list of suggestions (possibly null if none)
*/
- abstract public void getWords(final WordComposer composer,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo);
+ // TODO: pass more context than just the previous word, to enable better suggestions (n-gram
+ // and more)
+ abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo);
- /**
- * Searches for pairs in the bigram dictionary that matches the previous word and all the
- * possible words following are added through the callback object.
- * @param composer the key sequence to match
- * @param previousWord the word before
- * @param callback the callback object to send possible word following previous word
- */
- public void getBigrams(final WordComposer composer, final CharSequence previousWord,
- final WordCallback callback) {
- // empty base implementation
+ // The default implementation of this method ignores sessionId.
+ // Subclasses that want to use sessionId need to override this method.
+ public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) {
+ return getSuggestions(composer, prevWord, proximityInfo);
}
/**
@@ -115,4 +105,12 @@ public abstract class Dictionary {
public void close() {
// empty base implementation
}
+
+ /**
+ * Subclasses may override to indicate that this Dictionary is not yet properly initialized.
+ */
+
+ public boolean isInitialized() {
+ return true;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 1a05fcd86..4acab6b05 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -17,9 +17,11 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import android.util.Log;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -31,36 +33,44 @@ public class DictionaryCollection extends Dictionary {
private final String TAG = DictionaryCollection.class.getSimpleName();
protected final CopyOnWriteArrayList<Dictionary> mDictionaries;
- public DictionaryCollection() {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+ public DictionaryCollection(final String dictType) {
+ super(dictType);
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList();
}
- public DictionaryCollection(Dictionary... dictionaries) {
+ public DictionaryCollection(final String dictType, Dictionary... dictionaries) {
+ super(dictType);
if (null == dictionaries) {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList();
} else {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries);
mDictionaries.removeAll(Collections.singleton(null));
}
}
- public DictionaryCollection(Collection<Dictionary> dictionaries) {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ public DictionaryCollection(final String dictType, Collection<Dictionary> dictionaries) {
+ super(dictType);
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries);
mDictionaries.removeAll(Collections.singleton(null));
}
@Override
- public void getWords(final WordComposer composer, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
- for (final Dictionary dict : mDictionaries)
- dict.getWords(composer, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public void getBigrams(final WordComposer composer, final CharSequence previousWord,
- final WordCallback callback) {
- for (final Dictionary dict : mDictionaries)
- dict.getBigrams(composer, previousWord, callback);
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
+ if (dictionaries.isEmpty()) return null;
+ // To avoid creating unnecessary objects, we get the list out of the first
+ // dictionary and add the rest to it if not null, hence the get(0)
+ ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer,
+ prevWord, proximityInfo);
+ if (null == suggestions) suggestions = CollectionUtils.newArrayList();
+ final int length = dictionaries.size();
+ for (int i = 1; i < length; ++ i) {
+ final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
+ prevWord, proximityInfo);
+ if (null != sugg) suggestions.addAll(sugg);
+ }
+ return suggestions;
}
@Override
@@ -82,8 +92,9 @@ public class DictionaryCollection extends Dictionary {
return maxFreq;
}
- public boolean isEmpty() {
- return mDictionaries.isEmpty();
+ @Override
+ public boolean isInitialized() {
+ return !mDictionaries.isEmpty();
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index a22d73af7..cdd01d0c7 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -49,17 +49,18 @@ public class DictionaryFactory {
final Locale locale, final boolean useFullEditDistance) {
if (null == locale) {
Log.e(TAG, "No locale defined for dictionary");
- return new DictionaryCollection(createBinaryDictionary(context, locale));
+ return new DictionaryCollection(Dictionary.TYPE_MAIN,
+ createBinaryDictionary(context, locale));
}
- final LinkedList<Dictionary> dictList = new LinkedList<Dictionary>();
+ final LinkedList<Dictionary> dictList = CollectionUtils.newLinkedList();
final ArrayList<AssetFileAddress> assetFileList =
BinaryDictionaryGetter.getDictionaryFiles(locale, context);
if (null != assetFileList) {
for (final AssetFileAddress f : assetFileList) {
final BinaryDictionary binaryDictionary =
new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength,
- useFullEditDistance, locale);
+ useFullEditDistance, locale, Dictionary.TYPE_MAIN);
if (binaryDictionary.isValidDictionary()) {
dictList.add(binaryDictionary);
}
@@ -69,7 +70,7 @@ public class DictionaryFactory {
// If the list is empty, that means we should not use any dictionary (for example, the user
// explicitly disabled the main dictionary), so the following is okay. dictList is never
// null, but if for some reason it is, DictionaryCollection handles it gracefully.
- return new DictionaryCollection(dictList);
+ return new DictionaryCollection(Dictionary.TYPE_MAIN, dictList);
}
/**
@@ -112,7 +113,7 @@ public class DictionaryFactory {
return null;
}
return new BinaryDictionary(context, sourceDir, afd.getStartOffset(), afd.getLength(),
- false /* useFullEditDistance */, locale);
+ false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
} catch (android.content.res.Resources.NotFoundException e) {
Log.e(TAG, "Could not find the resource");
return null;
@@ -140,7 +141,7 @@ public class DictionaryFactory {
long startOffset, long length, final boolean useFullEditDistance, Locale locale) {
if (dictionary.isFile()) {
return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length,
- useFullEditDistance, locale);
+ useFullEditDistance, locale, Dictionary.TYPE_MAIN);
} else {
Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
return null;
diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
deleted file mode 100644
index 0f34d50bb..000000000
--- a/java/src/com/android/inputmethod/latin/EditingUtils.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * 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.android.inputmethod.latin;
-
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-
-import java.util.regex.Pattern;
-
-/**
- * Utility methods to deal with editing text through an InputConnection.
- */
-public class EditingUtils {
- /**
- * Number of characters we want to look back in order to identify the previous word
- */
- // Provision for a long word pair and a separator
- private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
- private static final int INVALID_CURSOR_POSITION = -1;
-
- private EditingUtils() {
- // Unintentional empty constructor for singleton.
- }
-
- private static int getCursorPosition(InputConnection connection) {
- if (null == connection) return INVALID_CURSOR_POSITION;
- final ExtractedText extracted = connection.getExtractedText(new ExtractedTextRequest(), 0);
- if (extracted == null) {
- return INVALID_CURSOR_POSITION;
- }
- return extracted.startOffset + extracted.selectionStart;
- }
-
- /**
- * @param connection connection to the current text field.
- * @param separators characters which may separate words
- * @return the word that surrounds the cursor, including up to one trailing
- * separator. For example, if the field contains "he|llo world", where |
- * represents the cursor, then "hello " will be returned.
- */
- public static String getWordAtCursor(InputConnection connection, String separators) {
- // getWordRangeAtCursor returns null if the connection is null
- Range r = getWordRangeAtCursor(connection, separators);
- return (r == null) ? null : r.mWord;
- }
-
- /**
- * Represents a range of text, relative to the current cursor position.
- */
- public static class Range {
- /** Characters before selection start */
- public final int mCharsBefore;
-
- /**
- * Characters after selection start, including one trailing word
- * separator.
- */
- public final int mCharsAfter;
-
- /** The actual characters that make up a word */
- public final String mWord;
-
- public Range(int charsBefore, int charsAfter, String word) {
- if (charsBefore < 0 || charsAfter < 0) {
- throw new IndexOutOfBoundsException();
- }
- this.mCharsBefore = charsBefore;
- this.mCharsAfter = charsAfter;
- this.mWord = word;
- }
- }
-
- private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
- if (connection == null || sep == null) {
- return null;
- }
- CharSequence before = connection.getTextBeforeCursor(1000, 0);
- CharSequence after = connection.getTextAfterCursor(1000, 0);
- if (before == null || after == null) {
- return null;
- }
-
- // Find first word separator before the cursor
- int start = before.length();
- while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
-
- // Find last word separator after the cursor
- int end = -1;
- while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
- // Nothing to do here.
- }
-
- int cursor = getCursorPosition(connection);
- if (start >= 0 && cursor + end <= after.length() + before.length()) {
- String word = before.toString().substring(start, before.length())
- + after.toString().substring(0, end);
- return new Range(before.length() - start, end, word);
- }
-
- return null;
- }
-
- private static boolean isWhitespace(int code, String whitespace) {
- return whitespace.contains(String.valueOf((char) code));
- }
-
- private static final Pattern spaceRegex = Pattern.compile("\\s+");
-
- public static CharSequence getPreviousWord(InputConnection connection,
- String sentenceSeperators) {
- //TODO: Should fix this. This could be slow!
- if (null == connection) return null;
- CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
- return getPreviousWord(prev, sentenceSeperators);
- }
-
- // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
- // Also, it won't return words that end in a separator.
- // Example :
- // "abc def|" -> abc
- // "abc def |" -> abc
- // "abc def. |" -> abc
- // "abc def . |" -> def
- // "abc|" -> null
- // "abc |" -> null
- // "abc. def|" -> null
- public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
- if (prev == null) return null;
- String[] w = spaceRegex.split(prev);
-
- // If we can't find two words, or we found an empty word, return null.
- if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
-
- // If ends in a separator, return null
- char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
- return w[w.length - 2];
- }
-
- public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
- if (null == connection) return null;
- final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
- return getThisWord(prev, sentenceSeperators);
- }
-
- // Get the word immediately before the cursor, even if there is whitespace between it and
- // the cursor - but not if there is punctuation.
- // Example :
- // "abc def|" -> def
- // "abc def |" -> def
- // "abc def. |" -> null
- // "abc def . |" -> null
- public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
- if (prev == null) return null;
- String[] w = spaceRegex.split(prev);
-
- // No word : return null
- if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
-
- // If ends in a separator, return null
- char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
- return w[w.length - 1];
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index c65404cbc..cdf5247de 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -19,6 +19,7 @@ import android.os.SystemClock;
import android.util.Log;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
import com.android.inputmethod.latin.makedict.FusionDictionary;
import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
@@ -61,7 +62,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* that filename.
*/
private static final HashMap<String, DictionaryController> sSharedDictionaryControllers =
- new HashMap<String, DictionaryController>();
+ CollectionUtils.newHashMap();
/** The application context. */
protected final Context mContext;
@@ -75,9 +76,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/** The expandable fusion dictionary used to generate the binary dictionary. */
private FusionDictionary mFusionDictionary;
- /** The dictionary type id. */
- public final int mDicTypeId;
-
/**
* The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
* dictionary instances with the same filename is supported, with access controlled by
@@ -123,11 +121,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* @param context The application context of the parent.
* @param filename The filename for this binary dictionary. Multiple dictionaries with the same
* filename is supported.
- * @param dictType The type of this dictionary.
+ * @param dictType the dictionary type, as a human-readable string
*/
public ExpandableBinaryDictionary(
- final Context context, final String filename, final int dictType) {
- mDicTypeId = dictType;
+ final Context context, final String filename, final String dictType) {
+ super(dictType);
mFilename = filename;
mContext = context;
mBinaryDictionary = null;
@@ -161,9 +159,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* the native side.
*/
public void clearFusionDictionary() {
+ final HashMap<String, String> attributes = CollectionUtils.newHashMap();
mFusionDictionary = new FusionDictionary(new Node(),
- new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
- false));
+ new FusionDictionary.DictionaryOptions(attributes, false, false));
}
/**
@@ -177,7 +175,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
mFusionDictionary.add(word, frequency, null);
} else {
// TODO: Do this in the subclass, with this class taking an arraylist.
- final ArrayList<WeightedString> shortcutTargets = new ArrayList<WeightedString>();
+ final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
mFusionDictionary.add(word, frequency, shortcutTargets);
}
@@ -194,46 +192,19 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
@Override
- public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
asyncReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- protected final void getWordsInner(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- // Ensure that there are no concurrent calls to getWords. If there are, do nothing and
- // return.
- if (mLocalDictionaryController.tryLock()) {
- try {
- if (mBinaryDictionary != null) {
- mBinaryDictionary.getWords(codes, prevWordForBigrams, callback, proximityInfo);
- }
- } finally {
- mLocalDictionaryController.unlock();
- }
- }
- }
-
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- asyncReloadDictionaryIfRequired();
- getBigramsInner(codes, previousWord, callback);
- }
-
- protected void getBigramsInner(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
if (mLocalDictionaryController.tryLock()) {
try {
if (mBinaryDictionary != null) {
- mBinaryDictionary.getBigrams(codes, previousWord, callback);
+ return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo);
}
} finally {
mLocalDictionaryController.unlock();
}
}
+ return null;
}
@Override
@@ -306,7 +277,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// Build the new binary dictionary
final BinaryDictionary newBinaryDictionary =
new BinaryDictionary(mContext, filename, 0, length, true /* useFullEditDistance */,
- null);
+ null, mDictType);
if (mBinaryDictionary != null) {
// Ensure all threads accessing the current dictionary have finished before swapping in
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index 34a92fd30..8a38d1e1b 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -17,10 +17,11 @@
package com.android.inputmethod.latin;
import android.content.Context;
+import android.text.TextUtils;
-import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
import java.util.ArrayList;
@@ -37,7 +38,6 @@ public class ExpandableDictionary extends Dictionary {
private Context mContext;
private char[] mWordBuilder = new char[BinaryDictionary.MAX_WORD_LENGTH];
- private int mDicTypeId;
private int mMaxDepth;
private int mInputLength;
@@ -151,11 +151,11 @@ public class ExpandableDictionary extends Dictionary {
private int[][] mCodes;
- public ExpandableDictionary(Context context, int dicTypeId) {
+ public ExpandableDictionary(final Context context, final String dictType) {
+ super(dictType);
mContext = context;
clearDictionary();
mCodes = new int[BinaryDictionary.MAX_WORD_LENGTH][];
- mDicTypeId = dicTypeId;
}
public void loadDictionary() {
@@ -230,7 +230,7 @@ public class ExpandableDictionary extends Dictionary {
childNode.mTerminal = true;
if (isShortcutOnly) {
if (null == childNode.mShortcutTargets) {
- childNode.mShortcutTargets = new ArrayList<char[]>();
+ childNode.mShortcutTargets = CollectionUtils.newArrayList();
}
childNode.mShortcutTargets.add(shortcutTarget.toCharArray());
} else {
@@ -247,27 +247,43 @@ public class ExpandableDictionary extends Dictionary {
}
@Override
- public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ if (reloadDictionaryIfRequired()) return null;
+ if (composer.size() > 1) {
+ if (composer.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
+ return null;
+ }
+ final ArrayList<SuggestedWordInfo> suggestions =
+ getWordsInner(composer, prevWord, proximityInfo);
+ return suggestions;
+ } else {
+ if (TextUtils.isEmpty(prevWord)) return null;
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+ runBigramReverseLookUp(prevWord, suggestions);
+ return suggestions;
+ }
+ }
+
+ // This reloads the dictionary if required, and returns whether it's currently updating its
+ // contents or not.
+ // @VisibleForTesting
+ boolean reloadDictionaryIfRequired() {
synchronized (mUpdatingLock) {
// If we need to update, start off a background task
if (mRequiresReload) startDictionaryLoadingTaskLocked();
- // Currently updating contacts, don't return any results.
- if (mUpdatingDictionary) return;
- }
- if (codes.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
- return;
+ return mUpdatingDictionary;
}
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
}
- protected final void getWordsInner(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
+ protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
mInputLength = codes.size();
if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
- final int[] xCoordinates = codes.getXCoordinates();
- final int[] yCoordinates = codes.getYCoordinates();
+ final InputPointers ips = codes.getInputPointers();
+ final int[] xCoordinates = ips.getXCoordinates();
+ final int[] yCoordinates = ips.getYCoordinates();
// Cache the codes so that we don't have to lookup an array list
for (int i = 0; i < mInputLength; i++) {
// TODO: Calculate proximity info here.
@@ -275,16 +291,17 @@ public class ExpandableDictionary extends Dictionary {
mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE];
}
final int x = xCoordinates != null && i < xCoordinates.length ?
- xCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+ xCoordinates[i] : Constants.NOT_A_COORDINATE;
final int y = xCoordinates != null && i < yCoordinates.length ?
- yCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+ yCoordinates[i] : Constants.NOT_A_COORDINATE;
proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]);
}
mMaxDepth = mInputLength * 3;
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, callback);
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, suggestions);
for (int i = 0; i < mInputLength; i++) {
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, callback);
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, suggestions);
}
+ return suggestions;
}
@Override
@@ -368,24 +385,27 @@ public class ExpandableDictionary extends Dictionary {
* @param word the word to insert, as an array of code points
* @param depth the depth of the node in the tree
* @param finalFreq the frequency for this word
+ * @param suggestions the suggestion collection to add the suggestions to
* @return whether there is still space for more words.
- * @see Dictionary.WordCallback#addWord(char[], int, int, int, int, int)
*/
private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth,
- final int finalFreq, final WordCallback callback) {
+ final int finalFreq, final ArrayList<SuggestedWordInfo> suggestions) {
if (finalFreq > 0 && !node.mShortcutOnly) {
- if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, Dictionary.UNIGRAM)) {
- return false;
- }
+ // Use KIND_CORRECTION always. This dictionary does not really have a notion of
+ // COMPLETION against CORRECTION; we could artificially add one by looking at
+ // the respective size of the typed word and the suggestion if it matters sometime
+ // in the future.
+ suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq,
+ SuggestedWordInfo.KIND_CORRECTION, mDictType));
+ if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false;
}
if (null != node.mShortcutTargets) {
final int length = node.mShortcutTargets.size();
for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) {
final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
- if (!callback.addWord(shortcut, 0, shortcut.length, finalFreq, mDicTypeId,
- Dictionary.UNIGRAM)) {
- return false;
- }
+ suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length),
+ finalFreq, SuggestedWordInfo.KIND_SHORTCUT, mDictType));
+ if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
}
}
return true;
@@ -408,12 +428,12 @@ public class ExpandableDictionary extends Dictionary {
* case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
* "wouldve", it could be matching "would've", so the depth will be one more than the
* inputIndex
- * @param callback the callback class for adding a word
+ * @param suggestions the list in which to add suggestions
*/
// TODO: Share this routine with the native code for BinaryDictionary
protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
final int depth, final boolean completion, int snr, int inputIndex, int skipPos,
- WordCallback callback) {
+ final ArrayList<SuggestedWordInfo> suggestions) {
final int count = roots.mLength;
final int codeSize = mInputLength;
// Optimization: Prune out words that are too long compared to how much was typed.
@@ -443,14 +463,14 @@ public class ExpandableDictionary extends Dictionary {
} else {
finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength);
}
- if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, callback)) {
+ if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, suggestions)) {
// No space left in the queue, bail out
return;
}
}
if (children != null) {
getWordsRec(children, codes, word, depth + 1, true, snr, inputIndex,
- skipPos, callback);
+ skipPos, suggestions);
}
} else if ((c == Keyboard.CODE_SINGLE_QUOTE
&& currentChars[0] != Keyboard.CODE_SINGLE_QUOTE) || depth == skipPos) {
@@ -458,7 +478,7 @@ public class ExpandableDictionary extends Dictionary {
word[depth] = c;
if (children != null) {
getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
- skipPos, callback);
+ skipPos, suggestions);
}
} else {
// Don't use alternatives if we're looking for missing characters
@@ -466,7 +486,7 @@ public class ExpandableDictionary extends Dictionary {
for (int j = 0; j < alternativesSize; j++) {
final int addedAttenuation = (j > 0 ? 1 : 2);
final int currentChar = currentChars[j];
- if (currentChar == KeyDetector.NOT_A_CODE) {
+ if (currentChar == Constants.NOT_A_CODE) {
break;
}
if (currentChar == lowerC || currentChar == c) {
@@ -483,7 +503,7 @@ public class ExpandableDictionary extends Dictionary {
snr * addedAttenuation, mInputLength);
}
if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq,
- callback)) {
+ suggestions)) {
// No space left in the queue, bail out
return;
}
@@ -491,12 +511,12 @@ public class ExpandableDictionary extends Dictionary {
if (children != null) {
getWordsRec(children, codes, word, depth + 1,
true, snr * addedAttenuation, inputIndex + 1,
- skipPos, callback);
+ skipPos, suggestions);
}
} else if (children != null) {
getWordsRec(children, codes, word, depth + 1,
false, snr * addedAttenuation, inputIndex + 1,
- skipPos, callback);
+ skipPos, suggestions);
}
}
}
@@ -514,8 +534,10 @@ public class ExpandableDictionary extends Dictionary {
/**
* Adds bigrams to the in-memory trie structure that is being used to retrieve any word
+ * @param word1 the first word of this bigram
+ * @param word2 the second word of this bigram
* @param frequency frequency for this bigram
- * @param addFrequency if true, it adds to current frequency, else it overwrites the old value
+ * @param fcp an instance of ForgettingCurveParams to use for decay policy
* @return returns the final bigram frequency
*/
private int setBigramAndGetFrequency(
@@ -528,7 +550,7 @@ public class ExpandableDictionary extends Dictionary {
Node secondWord = searchWord(mRoots, word2, 0, null);
LinkedList<NextWord> bigrams = firstWord.mNGrams;
if (bigrams == null || bigrams.size() == 0) {
- firstWord.mNGrams = new LinkedList<NextWord>();
+ firstWord.mNGrams = CollectionUtils.newLinkedList();
bigrams = firstWord.mNGrams;
} else {
for (NextWord nw : bigrams) {
@@ -580,32 +602,14 @@ public class ExpandableDictionary extends Dictionary {
return searchWord(childNode.mChildren, word, depth + 1, childNode);
}
- // @VisibleForTesting
- boolean reloadDictionaryIfRequired() {
- synchronized (mUpdatingLock) {
- // If we need to update, start off a background task
- if (mRequiresReload) startDictionaryLoadingTaskLocked();
- // Currently updating contacts, don't return any results.
- return mUpdatingDictionary;
- }
- }
-
private void runBigramReverseLookUp(final CharSequence previousWord,
- final WordCallback callback) {
+ final ArrayList<SuggestedWordInfo> suggestions) {
// Search for the lowercase version of the word only, because that's where bigrams
// store their sons.
Node prevWord = searchNode(mRoots, previousWord.toString().toLowerCase(), 0,
previousWord.length());
if (prevWord != null && prevWord.mNGrams != null) {
- reverseLookUp(prevWord.mNGrams, callback);
- }
- }
-
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- if (!reloadDictionaryIfRequired()) {
- runBigramReverseLookUp(previousWord, callback);
+ reverseLookUp(prevWord.mNGrams, suggestions);
}
}
@@ -633,11 +637,12 @@ public class ExpandableDictionary extends Dictionary {
/**
* reverseLookUp retrieves the full word given a list of terminal nodes and adds those words
- * through callback.
+ * to the suggestions list passed as an argument.
* @param terminalNodes list of terminal nodes we want to add
+ * @param suggestions the suggestion collection to add the word to
*/
private void reverseLookUp(LinkedList<NextWord> terminalNodes,
- final WordCallback callback) {
+ final ArrayList<SuggestedWordInfo> suggestions) {
Node node;
int freq;
for (NextWord nextWord : terminalNodes) {
@@ -648,11 +653,15 @@ public class ExpandableDictionary extends Dictionary {
--index;
mLookedUpString[index] = node.mCode;
node = node.mParent;
- } while (node != null);
-
- if (freq >= 0) {
- callback.addWord(mLookedUpString, index, BinaryDictionary.MAX_WORD_LENGTH - index,
- freq, mDicTypeId, Dictionary.BIGRAM);
+ } while (node != null && index > 0);
+
+ // If node is null, we have a word longer than MAX_WORD_LENGTH in the dictionary.
+ // It's a little unclear how this can happen, but just in case it does it's safer
+ // to ignore the word in this case.
+ if (freq >= 0 && node == null) {
+ suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index,
+ BinaryDictionary.MAX_WORD_LENGTH - index),
+ freq, SuggestedWordInfo.KIND_CORRECTION, mDictType));
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/ImfUtils.java b/java/src/com/android/inputmethod/latin/ImfUtils.java
index b882a4860..1461c0240 100644
--- a/java/src/com/android/inputmethod/latin/ImfUtils.java
+++ b/java/src/com/android/inputmethod/latin/ImfUtils.java
@@ -90,6 +90,13 @@ public class ImfUtils {
return false;
}
+ public static InputMethodSubtype getCurrentInputMethodSubtype(Context context,
+ InputMethodSubtype defaultSubtype) {
+ final InputMethodManager imm = getInputMethodManager(context);
+ final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
+ return (currentSubtype != null) ? currentSubtype : defaultSubtype;
+ }
+
public static boolean hasMultipleEnabledIMEsOrSubtypes(Context context,
final boolean shouldIncludeAuxiliarySubtypes) {
final InputMethodManager imm = getInputMethodManager(context);
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index 229ae2f3c..7bcda9bc4 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -29,11 +29,12 @@ public class InputAttributes {
final public boolean mInputTypeNoAutoCorrect;
final public boolean mIsSettingsSuggestionStripOn;
final public boolean mApplicationSpecifiedCompletionOn;
- final public int mEditorAction;
+ final private int mInputType;
public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) {
final int inputType = null != editorInfo ? editorInfo.inputType : 0;
final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
+ mInputType = inputType;
if (inputClass != InputType.TYPE_CLASS_TEXT) {
// If we are not looking at a TYPE_CLASS_TEXT field, the following strange
// cases may arise, so we do a couple sanity checks for them. If it's a
@@ -64,7 +65,7 @@ public class InputAttributes {
final boolean flagAutoComplete =
0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
- // Make sure that passwords are not displayed in {@link SuggestionsView}.
+ // Make sure that passwords are not displayed in {@link SuggestionStripView}.
if (InputTypeUtils.isPasswordInputType(inputType)
|| InputTypeUtils.isVisiblePasswordInputType(inputType)
|| InputTypeUtils.isEmailVariation(variation)
@@ -92,8 +93,10 @@ public class InputAttributes {
mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode;
}
- mEditorAction = (editorInfo == null) ? EditorInfo.IME_ACTION_UNSPECIFIED
- : editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+ }
+
+ public boolean isSameInputType(final EditorInfo editorInfo) {
+ return editorInfo.inputType == mInputType;
}
@SuppressWarnings("unused")
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
new file mode 100644
index 000000000..ff2feb51d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/InputPointers.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin;
+
+// TODO: This class is not thread-safe.
+public class InputPointers {
+ private final int mDefaultCapacity;
+ private final ResizableIntArray mXCoordinates;
+ private final ResizableIntArray mYCoordinates;
+ private final ResizableIntArray mPointerIds;
+ private final ResizableIntArray mTimes;
+
+ public InputPointers(int defaultCapacity) {
+ mDefaultCapacity = defaultCapacity;
+ mXCoordinates = new ResizableIntArray(defaultCapacity);
+ mYCoordinates = new ResizableIntArray(defaultCapacity);
+ mPointerIds = new ResizableIntArray(defaultCapacity);
+ mTimes = new ResizableIntArray(defaultCapacity);
+ }
+
+ public void addPointer(int index, int x, int y, int pointerId, int time) {
+ mXCoordinates.add(index, x);
+ mYCoordinates.add(index, y);
+ mPointerIds.add(index, pointerId);
+ mTimes.add(index, time);
+ }
+
+ public void addPointer(int x, int y, int pointerId, int time) {
+ mXCoordinates.add(x);
+ mYCoordinates.add(y);
+ mPointerIds.add(pointerId);
+ mTimes.add(time);
+ }
+
+ public void set(InputPointers ip) {
+ mXCoordinates.set(ip.mXCoordinates);
+ mYCoordinates.set(ip.mYCoordinates);
+ mPointerIds.set(ip.mPointerIds);
+ mTimes.set(ip.mTimes);
+ }
+
+ public void copy(InputPointers ip) {
+ mXCoordinates.copy(ip.mXCoordinates);
+ mYCoordinates.copy(ip.mYCoordinates);
+ mPointerIds.copy(ip.mPointerIds);
+ mTimes.copy(ip.mTimes);
+ }
+
+ /**
+ * Append the pointers in the specified {@link InputPointers} to the end of this.
+ * @param src the source {@link InputPointers} to read the data from.
+ * @param startPos the starting index of the pointers in {@code src}.
+ * @param length the number of pointers to be appended.
+ */
+ public void append(InputPointers src, int startPos, int length) {
+ if (length == 0) {
+ return;
+ }
+ mXCoordinates.append(src.mXCoordinates, startPos, length);
+ mYCoordinates.append(src.mYCoordinates, startPos, length);
+ mPointerIds.append(src.mPointerIds, startPos, length);
+ mTimes.append(src.mTimes, startPos, length);
+ }
+
+ /**
+ * Append the times, x-coordinates and y-coordinates in the specified {@link ResizableIntArray}
+ * to the end of this.
+ * @param pointerId the pointer id of the source.
+ * @param times the source {@link ResizableIntArray} to read the event times from.
+ * @param xCoordinates the source {@link ResizableIntArray} to read the x-coordinates from.
+ * @param yCoordinates the source {@link ResizableIntArray} to read the y-coordinates from.
+ * @param startPos the starting index of the data in {@code times} and etc.
+ * @param length the number of data to be appended.
+ */
+ public void append(int pointerId, ResizableIntArray times, ResizableIntArray xCoordinates,
+ ResizableIntArray yCoordinates, int startPos, int length) {
+ if (length == 0) {
+ return;
+ }
+ mXCoordinates.append(xCoordinates, startPos, length);
+ mYCoordinates.append(yCoordinates, startPos, length);
+ mPointerIds.fill(pointerId, mPointerIds.getLength(), length);
+ mTimes.append(times, startPos, length);
+ }
+
+ public void reset() {
+ final int defaultCapacity = mDefaultCapacity;
+ mXCoordinates.reset(defaultCapacity);
+ mYCoordinates.reset(defaultCapacity);
+ mPointerIds.reset(defaultCapacity);
+ mTimes.reset(defaultCapacity);
+ }
+
+ public int getPointerSize() {
+ return mXCoordinates.getLength();
+ }
+
+ public int[] getXCoordinates() {
+ return mXCoordinates.getPrimitiveArray();
+ }
+
+ public int[] getYCoordinates() {
+ return mYCoordinates.getPrimitiveArray();
+ }
+
+ public int[] getPointerIds() {
+ return mPointerIds.getPrimitiveArray();
+ }
+
+ public int[] getTimes() {
+ return mTimes.getPrimitiveArray();
+ }
+
+ @Override
+ public String toString() {
+ return "size=" + getPointerSize() + " id=" + mPointerIds + " time=" + mTimes
+ + " x=" + mXCoordinates + " y=" + mYCoordinates;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
index 0dcb811b5..c15f45345 100644
--- a/java/src/com/android/inputmethod/latin/InputView.java
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -24,7 +24,7 @@ import android.view.View;
import android.widget.LinearLayout;
public class InputView extends LinearLayout {
- private View mSuggestionsContainer;
+ private View mSuggestionStripContainer;
private View mKeyboardView;
private int mKeyboardTopPadding;
@@ -43,13 +43,13 @@ public class InputView extends LinearLayout {
@Override
protected void onFinishInflate() {
- mSuggestionsContainer = findViewById(R.id.suggestions_container);
+ mSuggestionStripContainer = findViewById(R.id.suggestions_container);
mKeyboardView = findViewById(R.id.keyboard_view);
}
@Override
public boolean dispatchTouchEvent(MotionEvent me) {
- if (mSuggestionsContainer.getVisibility() == VISIBLE
+ if (mSuggestionStripContainer.getVisibility() == VISIBLE
&& mKeyboardView.getVisibility() == VISIBLE
&& forwardTouchEvent(me)) {
return true;
@@ -57,7 +57,8 @@ public class InputView extends LinearLayout {
return super.dispatchTouchEvent(me);
}
- // The touch events that hit the top padding of keyboard should be forwarded to SuggestionsView.
+ // The touch events that hit the top padding of keyboard should be forwarded to
+ // {@link SuggestionStripView}.
private boolean forwardTouchEvent(MotionEvent me) {
final Rect rect = mInputViewRect;
this.getGlobalVisibleRect(rect);
@@ -96,7 +97,7 @@ public class InputView extends LinearLayout {
}
final Rect receivingRect = mEventReceivingRect;
- mSuggestionsContainer.getGlobalVisibleRect(receivingRect);
+ mSuggestionStripContainer.getGlobalVisibleRect(receivingRect);
final int translatedX = x - receivingRect.left;
final int translatedY;
if (y < forwardingLimitY) {
@@ -106,7 +107,7 @@ public class InputView extends LinearLayout {
translatedY = y - receivingRect.top;
}
me.setLocation(translatedX, translatedY);
- mSuggestionsContainer.dispatchTouchEvent(me);
+ mSuggestionStripContainer.dispatchTouchEvent(me);
return true;
}
}
diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/JniUtils.java
index 4808b867a..86a3826d8 100644
--- a/java/src/com/android/inputmethod/latin/JniUtils.java
+++ b/java/src/com/android/inputmethod/latin/JniUtils.java
@@ -31,11 +31,7 @@ public class JniUtils {
try {
System.loadLibrary(JniLibName.JNI_LIB_NAME);
} catch (UnsatisfiedLinkError ule) {
- Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME);
- if (LatinImeLogger.sDBG) {
- throw new RuntimeException(
- "Could not load native library " + JniLibName.JNI_LIB_NAME);
- }
+ Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME, ule);
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
index 4e1f5fe92..bb39ce4f7 100644
--- a/java/src/com/android/inputmethod/latin/LastComposedWord.java
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -41,26 +41,26 @@ public class LastComposedWord {
public static final int NOT_A_SEPARATOR = -1;
public final int[] mPrimaryKeyCodes;
- public final int[] mXCoordinates;
- public final int[] mYCoordinates;
public final String mTypedWord;
public final String mCommittedWord;
public final int mSeparatorCode;
public final CharSequence mPrevWord;
+ public final InputPointers mInputPointers = new InputPointers(BinaryDictionary.MAX_WORD_LENGTH);
private boolean mActive;
public static final LastComposedWord NOT_A_COMPOSED_WORD =
- new LastComposedWord(null, null, null, "", "", NOT_A_SEPARATOR, null);
+ new LastComposedWord(null, null, "", "", NOT_A_SEPARATOR, null);
// Warning: this is using the passed objects as is and fully expects them to be
// immutable. Do not fiddle with their contents after you passed them to this constructor.
- public LastComposedWord(final int[] primaryKeyCodes, final int[] xCoordinates,
- final int[] yCoordinates, final String typedWord, final String committedWord,
+ public LastComposedWord(final int[] primaryKeyCodes, final InputPointers inputPointers,
+ final String typedWord, final String committedWord,
final int separatorCode, final CharSequence prevWord) {
mPrimaryKeyCodes = primaryKeyCodes;
- mXCoordinates = xCoordinates;
- mYCoordinates = yCoordinates;
+ if (inputPointers != null) {
+ mInputPointers.copy(inputPointers);
+ }
mTypedWord = typedWord;
mCommittedWord = committedWord;
mSeparatorCode = separatorCode;
@@ -73,10 +73,10 @@ public class LastComposedWord {
}
public boolean canRevertCommit() {
- return mActive && !TextUtils.isEmpty(mCommittedWord);
+ return mActive && !TextUtils.isEmpty(mCommittedWord) && !didCommitTypedWord();
}
- public boolean didCommitTypedWord() {
+ private boolean didCommitTypedWord() {
return TextUtils.equals(mTypedWord, mCommittedWord);
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 97e898af9..df200cd0e 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -20,6 +20,7 @@ import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
+import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -38,7 +39,6 @@ import android.os.Debug;
import android.os.IBinder;
import android.os.Message;
import android.os.SystemClock;
-import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.text.TextUtils;
@@ -48,31 +48,31 @@ import android.util.Printer;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
-import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
-import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
import com.android.inputmethod.compat.CompatUtils;
import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
import com.android.inputmethod.compat.SuggestionSpanUtils;
+import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.KeyboardView;
-import com.android.inputmethod.keyboard.LatinKeyboardView;
+import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.latin.suggestions.SuggestionsView;
+import com.android.inputmethod.latin.suggestions.SuggestionStripView;
+import com.android.inputmethod.research.ResearchLogger;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -83,7 +83,8 @@ import java.util.Locale;
* Input method implementation for Qwerty'ish keyboard.
*/
public class LatinIME extends InputMethodService implements KeyboardActionListener,
- SuggestionsView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener {
+ SuggestionStripView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener,
+ Suggest.SuggestInitializationListener {
private static final String TAG = LatinIME.class.getSimpleName();
private static final boolean TRACE = false;
private static boolean DEBUG;
@@ -103,27 +104,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
private static final String SCHEME_PACKAGE = "package";
- /** Whether to use the binary version of the contacts dictionary */
- public static final boolean USE_BINARY_CONTACTS_DICTIONARY = true;
-
- /** Whether to use the binary version of the user dictionary */
- public static final boolean USE_BINARY_USER_DICTIONARY = true;
-
- // TODO: migrate this to SettingsValues
- private int mSuggestionVisibility;
- private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
- = R.string.prefs_suggestion_visibility_show_value;
- private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
- = R.string.prefs_suggestion_visibility_show_only_portrait_value;
- private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
- = R.string.prefs_suggestion_visibility_hide_value;
-
- private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
- SUGGESTION_VISIBILILTY_SHOW_VALUE,
- SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
- SUGGESTION_VISIBILILTY_HIDE_VALUE
- };
-
private static final int SPACE_STATE_NONE = 0;
// Double space: the state where the user pressed space twice quickly, which LatinIME
// resolved as period-space. Undoing this converts the period to a space.
@@ -143,13 +123,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Current space state of the input method. This can be any of the above constants.
private int mSpaceState;
- private SettingsValues mSettingsValues;
- private InputAttributes mInputAttributes;
+ private SettingsValues mCurrentSettings;
private View mExtractArea;
private View mKeyPreviewBackingView;
private View mSuggestionsContainer;
- private SuggestionsView mSuggestionsView;
+ private SuggestionStripView mSuggestionStripView;
/* package for tests */ Suggest mSuggest;
private CompletionInfo[] mApplicationSpecifiedCompletions;
private ApplicationInfo mTargetApplicationInfo;
@@ -162,15 +141,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private boolean mShouldSwitchToLastSubtype = true;
private boolean mIsMainDictionaryAvailable;
- // TODO: revert this back to the concrete class after transition.
- private Dictionary mUserDictionary;
+ private UserBinaryDictionary mUserDictionary;
private UserHistoryDictionary mUserHistoryDictionary;
private boolean mIsUserDictionaryAvailable;
private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
private WordComposer mWordComposer = new WordComposer();
-
- private int mCorrectionMode;
+ private RichInputConnection mConnection = new RichInputConnection(this);
// Keep track of the last selection range to decide if we need to show word alternatives
private static final int NOT_A_CURSOR_POSITION = -1;
@@ -199,18 +176,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private AlertDialog mOptionsDialog;
+ private final boolean mIsHardwareAcceleratedDrawingEnabled;
+
public final UIHandler mHandler = new UIHandler(this);
public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
- private static final int MSG_UPDATE_SHIFT_STATE = 1;
- private static final int MSG_SPACE_TYPED = 4;
- private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
- private static final int MSG_PENDING_IMS_CALLBACK = 6;
- private static final int MSG_UPDATE_SUGGESTIONS = 7;
+ private static final int MSG_UPDATE_SHIFT_STATE = 0;
+ private static final int MSG_PENDING_IMS_CALLBACK = 1;
+ private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
private int mDelayUpdateSuggestions;
private int mDelayUpdateShiftState;
private long mDoubleSpacesTurnIntoPeriodTimeout;
+ private long mDoubleSpaceTimerStart;
public UIHandler(LatinIME outerInstance) {
super(outerInstance);
@@ -231,29 +209,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final LatinIME latinIme = getOuterInstance();
final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
switch (msg.what) {
- case MSG_UPDATE_SUGGESTIONS:
- latinIme.updateSuggestions();
+ case MSG_UPDATE_SUGGESTION_STRIP:
+ latinIme.updateSuggestionStrip();
break;
case MSG_UPDATE_SHIFT_STATE:
switcher.updateShiftState();
break;
- case MSG_SET_BIGRAM_PREDICTIONS:
- latinIme.updateBigramPredictions();
- break;
}
}
- public void postUpdateSuggestions() {
- removeMessages(MSG_UPDATE_SUGGESTIONS);
- sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
+ public void postUpdateSuggestionStrip() {
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
}
- public void cancelUpdateSuggestions() {
- removeMessages(MSG_UPDATE_SUGGESTIONS);
+ public void cancelUpdateSuggestionStrip() {
+ removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
}
public boolean hasPendingUpdateSuggestions() {
- return hasMessages(MSG_UPDATE_SUGGESTIONS);
+ return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
}
public void postUpdateShiftState() {
@@ -265,26 +239,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
removeMessages(MSG_UPDATE_SHIFT_STATE);
}
- public void postUpdateBigramPredictions() {
- removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
- sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
- }
-
- public void cancelUpdateBigramPredictions() {
- removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
- }
-
public void startDoubleSpacesTimer() {
- removeMessages(MSG_SPACE_TYPED);
- sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout);
+ mDoubleSpaceTimerStart = SystemClock.uptimeMillis();
}
public void cancelDoubleSpacesTimer() {
- removeMessages(MSG_SPACE_TYPED);
+ mDoubleSpaceTimerStart = 0;
}
public boolean isAcceptingDoubleSpaces() {
- return hasMessages(MSG_SPACE_TYPED);
+ return SystemClock.uptimeMillis() - mDoubleSpaceTimerStart
+ < mDoubleSpacesTurnIntoPeriodTimeout;
}
// Working variables for the following methods.
@@ -385,6 +350,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
super();
mSubtypeSwitcher = SubtypeSwitcher.getInstance();
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
+ mIsHardwareAcceleratedDrawingEnabled =
+ InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
+ Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
}
@Override
@@ -393,7 +361,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mPrefs = prefs;
LatinImeLogger.init(this, prefs);
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.init(this, prefs);
+ ResearchLogger.getInstance().init(this, prefs);
}
InputMethodManagerCompatWrapper.init(this);
SubtypeSwitcher.init(this);
@@ -411,22 +379,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
loadSettings();
- ImfUtils.setAdditionalInputMethodSubtypes(this, mSettingsValues.getAdditionalSubtypes());
+ ImfUtils.setAdditionalInputMethodSubtypes(this, mCurrentSettings.getAdditionalSubtypes());
- // TODO: remove the following when it's not needed by updateCorrectionMode() any more
- mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
- updateCorrectionMode();
-
- Utils.GCUtils.getInstance().reset();
- boolean tryGC = true;
- for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
- try {
- initSuggest();
- tryGC = false;
- } catch (OutOfMemoryError e) {
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
- }
- }
+ initSuggest();
mDisplayOrientation = res.getConfiguration().orientation;
@@ -450,46 +405,58 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// Has to be package-visible for unit tests
- /* package */ void loadSettings() {
+ /* package for test */
+ void loadSettings() {
// Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
// is not guaranteed. It may even be called at the same time on a different thread.
if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ final InputAttributes inputAttributes =
+ new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
@Override
protected SettingsValues job(Resources res) {
- return new SettingsValues(mPrefs, LatinIME.this);
+ return new SettingsValues(mPrefs, inputAttributes, LatinIME.this);
}
};
- mSettingsValues = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
- mFeedbackManager = new AudioAndHapticFeedbackManager(this, mSettingsValues);
+ mCurrentSettings = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
+ mFeedbackManager = new AudioAndHapticFeedbackManager(this, mCurrentSettings);
resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
}
+ // Note that this method is called from a non-UI thread.
+ @Override
+ public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable) {
+ mIsMainDictionaryAvailable = isMainDictionaryAvailable;
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
+ }
+ }
+
private void initSuggest() {
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
final String localeStr = subtypeLocale.toString();
- final Dictionary oldContactsDictionary;
+ final ContactsBinaryDictionary oldContactsDictionary;
if (mSuggest != null) {
oldContactsDictionary = mSuggest.getContactsDictionary();
mSuggest.close();
} else {
oldContactsDictionary = null;
}
- mSuggest = new Suggest(this, subtypeLocale);
- if (mSettingsValues.mAutoCorrectEnabled) {
- mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+ mSuggest = new Suggest(this /* Context */, subtypeLocale,
+ this /* SuggestInitializationListener */);
+ if (mCurrentSettings.mCorrectionEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
}
mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
-
- if (USE_BINARY_USER_DICTIONARY) {
- mUserDictionary = new UserBinaryDictionary(this, localeStr);
- mIsUserDictionaryAvailable = ((UserBinaryDictionary)mUserDictionary).isEnabled();
- } else {
- mUserDictionary = new UserDictionary(this, localeStr);
- mIsUserDictionaryAvailable = ((UserDictionary)mUserDictionary).isEnabled();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().initSuggest(mSuggest);
}
+
+ mUserDictionary = new UserBinaryDictionary(this, localeStr);
+ mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
mSuggest.setUserDictionary(mUserDictionary);
resetContactsDictionary(oldContactsDictionary);
@@ -497,44 +464,43 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
// is not guaranteed. It may even be called at the same time on a different thread.
if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
- mUserHistoryDictionary = UserHistoryDictionary.getInstance(
- this, localeStr, Suggest.DIC_USER_HISTORY, mPrefs);
+ mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, mPrefs);
mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
}
/**
* Resets the contacts dictionary in mSuggest according to the user settings.
*
- * This method takes an optional contacts dictionary to use. Since the contacts dictionary
- * does not depend on the locale, it can be reused across different instances of Suggest.
- * The dictionary will also be opened or closed as necessary depending on the settings.
+ * This method takes an optional contacts dictionary to use when the locale hasn't changed
+ * since the contacts dictionary can be opened or closed as necessary depending on the settings.
*
* @param oldContactsDictionary an optional dictionary to use, or null
*/
- private void resetContactsDictionary(final Dictionary oldContactsDictionary) {
- final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
+ private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
+ final boolean shouldSetDictionary = (null != mSuggest && mCurrentSettings.mUseContactsDict);
- final Dictionary dictionaryToUse;
+ final ContactsBinaryDictionary dictionaryToUse;
if (!shouldSetDictionary) {
// Make sure the dictionary is closed. If it is already closed, this is a no-op,
// so it's safe to call it anyways.
if (null != oldContactsDictionary) oldContactsDictionary.close();
dictionaryToUse = null;
- } else if (null != oldContactsDictionary) {
- // Make sure the old contacts dictionary is opened. If it is already open, this is a
- // no-op, so it's safe to call it anyways.
- if (USE_BINARY_CONTACTS_DICTIONARY) {
- ((ContactsBinaryDictionary)oldContactsDictionary).reopen(this);
- } else {
- ((ContactsDictionary)oldContactsDictionary).reopen(this);
- }
- dictionaryToUse = oldContactsDictionary;
} else {
- if (USE_BINARY_CONTACTS_DICTIONARY) {
- dictionaryToUse = new ContactsBinaryDictionary(this, Suggest.DIC_CONTACTS,
- mSubtypeSwitcher.getCurrentSubtypeLocale());
+ final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+ if (null != oldContactsDictionary) {
+ if (!oldContactsDictionary.mLocale.equals(locale)) {
+ // If the locale has changed then recreate the contacts dictionary. This
+ // allows locale dependent rules for handling bigram name predictions.
+ oldContactsDictionary.close();
+ dictionaryToUse = new ContactsBinaryDictionary(this, locale);
+ } else {
+ // Make sure the old contacts dictionary is opened. If it is already open,
+ // this is a no-op, so it's safe to call it anyways.
+ oldContactsDictionary.reopen(this);
+ dictionaryToUse = oldContactsDictionary;
+ }
} else {
- dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
+ dictionaryToUse = new ContactsBinaryDictionary(this, locale);
}
}
@@ -545,7 +511,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
/* package private */ void resetSuggestMainDict() {
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- mSuggest.resetMainDict(this, subtypeLocale);
+ mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
}
@@ -564,23 +530,28 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onConfigurationChanged(Configuration conf) {
- mSubtypeSwitcher.onConfigurationChanged(conf);
+ // System locale has been changed. Needs to reload keyboard.
+ if (mSubtypeSwitcher.onConfigurationChanged(conf, this)) {
+ loadKeyboard();
+ }
// If orientation changed while predicting, commit the change
if (mDisplayOrientation != conf.orientation) {
mDisplayOrientation = conf.orientation;
mHandler.startOrientationChanging();
- final InputConnection ic = getCurrentInputConnection();
- commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
- if (ic != null) ic.finishComposingText(); // For voice input
- if (isShowingOptionDialog())
+ mConnection.beginBatchEdit();
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ mConnection.finishComposingText();
+ mConnection.endBatchEdit();
+ if (isShowingOptionDialog()) {
mOptionsDialog.dismiss();
+ }
}
super.onConfigurationChanged(conf);
}
@Override
public View onCreateInputView() {
- return mKeyboardSwitcher.onCreateInputView();
+ return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
}
@Override
@@ -590,9 +561,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
.findViewById(android.R.id.extractArea);
mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
- mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
- if (mSuggestionsView != null)
- mSuggestionsView.setListener(this, view);
+ mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
+ if (mSuggestionStripView != null)
+ mSuggestionStripView.setListener(this, view);
if (LatinImeLogger.sVISUALDEBUG) {
mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
}
@@ -629,6 +600,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
// is not guaranteed. It may even be called at the same time on a different thread.
mSubtypeSwitcher.updateSubtype(subtype);
+ loadKeyboard();
}
private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) {
@@ -639,7 +611,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
super.onStartInputView(editorInfo, restarting);
final KeyboardSwitcher switcher = mKeyboardSwitcher;
- LatinKeyboardView inputView = switcher.getKeyboardView();
+ final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
if (editorInfo == null) {
Log.e(TAG, "Null EditorInfo in onStartInputView()");
@@ -682,84 +654,130 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
LatinImeLogger.onStartInputView(editorInfo);
// In landscape mode, this method gets called without the input view being created.
- if (inputView == null) {
+ if (mainKeyboardView == null) {
return;
}
// Forward this event to the accessibility utilities, if enabled.
final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
if (accessUtils.isTouchExplorationEnabled()) {
- accessUtils.onStartInputViewInternal(editorInfo, restarting);
+ accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
}
- mSubtypeSwitcher.updateParametersOnStartInputView();
+ final boolean selectionChanged = mLastSelectionStart != editorInfo.initialSelStart
+ || mLastSelectionEnd != editorInfo.initialSelEnd;
+ final boolean inputTypeChanged = !mCurrentSettings.isSameInputType(editorInfo);
+ final boolean isDifferentTextField = !restarting || inputTypeChanged;
+ if (isDifferentTextField) {
+ final boolean currentSubtypeEnabled = mSubtypeSwitcher
+ .updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled();
+ if (!currentSubtypeEnabled) {
+ // Current subtype is disabled. Needs to update subtype and keyboard.
+ final InputMethodSubtype newSubtype = ImfUtils.getCurrentInputMethodSubtype(
+ this, mSubtypeSwitcher.getNoLanguageSubtype());
+ mSubtypeSwitcher.updateSubtype(newSubtype);
+ loadKeyboard();
+ }
+ }
// The EditorInfo might have a flag that affects fullscreen mode.
// Note: This call should be done by InputMethodService?
updateFullscreenMode();
- mLastSelectionStart = editorInfo.initialSelStart;
- mLastSelectionEnd = editorInfo.initialSelEnd;
- mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
mApplicationSpecifiedCompletions = null;
- inputView.closing();
- mEnteredText = null;
- resetComposingState(true /* alsoResetLastComposedWord */);
- mDeleteCount = 0;
- mSpaceState = SPACE_STATE_NONE;
-
- loadSettings();
- updateCorrectionMode();
- updateSuggestionVisibility(mResources);
+ if (isDifferentTextField || selectionChanged) {
+ // If the selection changed, we reset the input state. Essentially, we come here with
+ // restarting == true when the app called setText() or similar. We should reset the
+ // state if the app set the text to something else, but keep it if it set a suggestion
+ // or something.
+ mEnteredText = null;
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ mDeleteCount = 0;
+ mSpaceState = SPACE_STATE_NONE;
- if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
- mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+ if (mSuggestionStripView != null) {
+ mSuggestionStripView.clear();
+ }
}
- switcher.loadKeyboard(editorInfo, mSettingsValues);
+ if (isDifferentTextField) {
+ mainKeyboardView.closing();
+ loadSettings();
+
+ if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
+ }
- if (mSuggestionsView != null)
- mSuggestionsView.clear();
+ switcher.loadKeyboard(editorInfo, mCurrentSettings);
+ }
setSuggestionStripShownInternal(
isSuggestionsStripVisible(), /* needsInputViewShown */ false);
- // Delay updating suggestions because keyboard input view may not be shown at this point.
- mHandler.postUpdateSuggestions();
+
+ mLastSelectionStart = editorInfo.initialSelStart;
+ mLastSelectionEnd = editorInfo.initialSelEnd;
+ // If we come here something in the text state is very likely to have changed.
+ // We should update the shift state regardless of whether we are restarting or not, because
+ // this is not perceived as a layout change that may be disruptive like we may have with
+ // switcher.loadKeyboard; in apps like Talk, we come here when the text is sent and the
+ // field gets emptied and we need to re-evaluate the shift state, but not the whole layout
+ // which would be disruptive.
+ mKeyboardSwitcher.updateShiftState();
+
+ mHandler.cancelUpdateSuggestionStrip();
mHandler.cancelDoubleSpacesTimer();
- inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
- mSettingsValues.mKeyPreviewPopupDismissDelay);
- inputView.setProximityCorrectionEnabled(true);
+ mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
+ mainKeyboardView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
+ mCurrentSettings.mKeyPreviewPopupDismissDelay);
+ mainKeyboardView.setGestureHandlingEnabledByUser(mCurrentSettings.mGestureInputEnabled);
+ mainKeyboardView.setGesturePreviewMode(mCurrentSettings.mGesturePreviewTrailEnabled,
+ mCurrentSettings.mGestureFloatingPreviewTextEnabled);
if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
}
+ // Callback for the TargetApplicationGetter
+ @Override
public void onTargetApplicationKnown(final ApplicationInfo info) {
mTargetApplicationInfo = info;
}
@Override
public void onWindowHidden() {
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd,
+ getCurrentInputConnection());
+ }
super.onWindowHidden();
- KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) inputView.closing();
+ final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.closing();
+ }
}
private void onFinishInputInternal() {
super.onFinishInput();
LatinImeLogger.commit();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().latinIME_onFinishInputInternal();
+ }
- KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) inputView.closing();
+ final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.closing();
+ }
}
private void onFinishInputViewInternal(boolean finishingInput) {
super.onFinishInputView(finishingInput);
mKeyboardSwitcher.onFinishInputView();
- KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) inputView.cancelAllMessages();
+ final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.cancelAllMessages();
+ }
// Remove pending messages related to update suggestions
- mHandler.cancelUpdateSuggestions();
+ mHandler.cancelUpdateSuggestionStrip();
}
@Override
@@ -768,7 +786,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
int composingSpanStart, int composingSpanEnd) {
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
composingSpanStart, composingSpanEnd);
-
if (DEBUG) {
Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
+ ", ose=" + oldSelEnd
@@ -780,9 +797,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
+ ", ce=" + composingSpanEnd);
}
if (ProductionFlag.IS_EXPERIMENTAL) {
+ final boolean expectingUpdateSelectionFromLogger =
+ ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
- composingSpanEnd);
+ composingSpanEnd, mExpectingUpdateSelection,
+ expectingUpdateSelectionFromLogger, mConnection);
+ if (expectingUpdateSelectionFromLogger) {
+ // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
+ return;
+ }
}
// TODO: refactor the following code to be less contrived.
@@ -841,7 +865,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
@Override
public void onExtractedTextClicked() {
- if (isSuggestionsRequested()) return;
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
super.onExtractedTextClicked();
}
@@ -857,7 +881,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
@Override
public void onExtractedCursorMovement(int dx, int dy) {
- if (isSuggestionsRequested()) return;
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
super.onExtractedCursorMovement(dx, dy);
}
@@ -885,43 +909,45 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
- }
- if (mInputAttributes.mApplicationSpecifiedCompletionOn) {
- mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
- if (applicationSpecifiedCompletions == null) {
- clearSuggestions();
- return;
+ if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
+ mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+ if (applicationSpecifiedCompletions == null) {
+ clearSuggestionStrip();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onDisplayCompletions(null);
}
+ return;
+ }
- final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
- SuggestedWords.getFromApplicationSpecifiedCompletions(
- applicationSpecifiedCompletions);
- final SuggestedWords suggestedWords = new SuggestedWords(
- applicationSuggestedWords,
- false /* typedWordValid */,
- false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
- false /* isPunctuationSuggestions */,
- false /* isObsoleteSuggestions */,
- false /* isPrediction */);
- // When in fullscreen mode, show completions generated by the application
- final boolean isAutoCorrection = false;
- setSuggestions(suggestedWords, isAutoCorrection);
- setAutoCorrectionIndicator(isAutoCorrection);
- // TODO: is this the right thing to do? What should we auto-correct to in
- // this case? This says to keep whatever the user typed.
- mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
- setSuggestionStripShown(true);
+ final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
+ SuggestedWords.getFromApplicationSpecifiedCompletions(
+ applicationSpecifiedCompletions);
+ final SuggestedWords suggestedWords = new SuggestedWords(
+ applicationSuggestedWords,
+ false /* typedWordValid */,
+ false /* hasAutoCorrectionCandidate */,
+ false /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
+ // When in fullscreen mode, show completions generated by the application
+ final boolean isAutoCorrection = false;
+ setSuggestionStrip(suggestedWords, isAutoCorrection);
+ setAutoCorrectionIndicator(isAutoCorrection);
+ // TODO: is this the right thing to do? What should we auto-correct to in
+ // this case? This says to keep whatever the user typed.
+ mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
+ setSuggestionStripShown(true);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
}
}
private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
// TODO: Modify this if we support suggestions with hard keyboard
if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
- final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
- final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ final boolean inputViewShown = (mainKeyboardView != null)
+ ? mainKeyboardView.isShown() : false;
final boolean shouldShowSuggestions = shown
&& (needsInputViewShown ? inputViewShown : true);
if (isFullscreenMode()) {
@@ -944,11 +970,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return currentHeight;
}
- final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
- if (keyboardView == null) {
+ final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView == null) {
return 0;
}
- final int keyboardHeight = keyboardView.getHeight();
+ final int keyboardHeight = mainKeyboardView.getHeight();
final int suggestionsHeight = mSuggestionsContainer.getHeight();
final int displayHeight = mResources.getDisplayMetrics().heightPixels;
final Rect rect = new Rect();
@@ -958,7 +984,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
- keyboardHeight;
final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
- params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
+ params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
mKeyPreviewBackingView.setLayoutParams(params);
return params.height;
}
@@ -966,9 +992,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onComputeInsets(InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
- final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView == null || mSuggestionsContainer == null)
+ final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView == null || mSuggestionsContainer == null) {
return;
+ }
final int adjustedBackingHeight = getAdjustedBackingViewHeight();
final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
@@ -981,13 +1008,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
int touchY = extraHeight;
// Need to set touchable region only if input view is being shown
- final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
- if (keyboardView != null && keyboardView.isShown()) {
+ if (mainKeyboardView.isShown()) {
if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
touchY -= suggestionsHeight;
}
- final int touchWidth = inputView.getWidth();
- final int touchHeight = inputView.getHeight() + extraHeight
+ final int touchWidth = mainKeyboardView.getWidth();
+ final int touchHeight = mainKeyboardView.getHeight() + extraHeight
// Extend touchable region below the keyboard.
+ EXTENDED_TOUCHABLE_REGION_HEIGHT;
outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
@@ -1001,7 +1027,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public boolean onEvaluateFullscreenMode() {
// Reread resource value here, because this method is called by framework anytime as needed.
final boolean isFullscreenModeAllowed =
- mSettingsValues.isFullscreenModeAllowed(getResources());
+ mCurrentSettings.isFullscreenModeAllowed(getResources());
return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
}
@@ -1016,15 +1042,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// This will reset the whole input state to the starting state. It will clear
- // the composing word, reset the last composed word, tell the inputconnection
- // and the composingStateManager about it.
+ // the composing word, reset the last composed word, tell the inputconnection about it.
private void resetEntireInputState() {
resetComposingState(true /* alsoResetLastComposedWord */);
- updateSuggestions();
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.finishComposingText();
- }
+ clearSuggestionStrip();
+ mConnection.finishComposingText();
}
private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1033,26 +1055,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
}
- public void commitTyped(final InputConnection ic, final int separatorCode) {
+ private void commitTyped(final int separatorCode) {
if (!mWordComposer.isComposingWord()) return;
final CharSequence typedWord = mWordComposer.getTypedWord();
if (typedWord.length() > 0) {
- if (ic != null) {
- ic.commitText(typedWord, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(typedWord);
- }
- }
+ mConnection.commitText(typedWord, 1);
final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
mLastComposedWord = mWordComposer.commitWord(
LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
separatorCode, prevWord);
}
- updateSuggestions();
}
+ // Called from the KeyboardSwitcher which needs to know auto caps state to display
+ // the right layout.
public int getCurrentAutoCapsState() {
- if (!mSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+ if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
final EditorInfo ei = getCurrentInputEditorInfo();
if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
@@ -1070,49 +1088,50 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// unless needed.
if (mWordComposer.isComposingWord()) return Constants.TextUtils.CAP_MODE_OFF;
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return Constants.TextUtils.CAP_MODE_OFF;
// TODO: This blocking IPC call is heavy. Consider doing this without using IPC calls.
// Note: getCursorCapsMode() returns the current capitalization mode that is any
// combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none
// of them.
- return ic.getCursorCapsMode(inputType);
+ return mConnection.getCursorCapsMode(inputType);
}
- // "ic" may be null
- private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
- if (null == ic) return;
- CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
+ // Factor in auto-caps and manual caps and compute the current caps mode.
+ private int getActualCapsMode() {
+ final int manual = mKeyboardSwitcher.getManualCapsMode();
+ if (manual != WordComposer.CAPS_MODE_OFF) return manual;
+ final int auto = getCurrentAutoCapsState();
+ if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
+ return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
+ }
+ if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
+ return WordComposer.CAPS_MODE_OFF;
+ }
+
+ private void swapSwapperAndSpace() {
+ CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
// It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
if (lastTwo != null && lastTwo.length() == 2
&& lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
- ic.deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
- ic.commitText(lastTwo.charAt(1) + " ", 1);
+ mConnection.deleteSurroundingText(2, 0);
+ mConnection.commitText(lastTwo.charAt(1) + " ", 1);
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
+ ResearchLogger.latinIME_swapSwapperAndSpace();
}
mKeyboardSwitcher.updateShiftState();
}
}
- private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
- if (mCorrectionMode == Suggest.CORRECTION_NONE) return false;
- if (ic == null) return false;
- final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
+ private boolean maybeDoubleSpace() {
+ if (!mCurrentSettings.mCorrectionEnabled) return false;
+ if (!mHandler.isAcceptingDoubleSpaces()) return false;
+ final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
if (lastThree != null && lastThree.length() == 3
&& canBeFollowedByPeriod(lastThree.charAt(0))
&& lastThree.charAt(1) == Keyboard.CODE_SPACE
- && lastThree.charAt(2) == Keyboard.CODE_SPACE
- && mHandler.isAcceptingDoubleSpaces()) {
+ && lastThree.charAt(2) == Keyboard.CODE_SPACE) {
mHandler.cancelDoubleSpacesTimer();
- ic.deleteSurroundingText(2, 0);
- ic.commitText(". ", 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_doubleSpaceAutoPeriod();
- }
+ mConnection.deleteSurroundingText(2, 0);
+ mConnection.commitText(". ", 1);
mKeyboardSwitcher.updateShiftState();
return true;
}
@@ -1131,29 +1150,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|| codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
}
- // "ic" may be null
- private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
- if (ic == null) return;
- final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
- if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
- ic.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
- }
- }
-
+ // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
+ // pressed.
@Override
- public boolean addWordToDictionary(String word) {
- if (USE_BINARY_USER_DICTIONARY) {
- ((UserBinaryDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
- } else {
- ((UserDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
- }
- // Suggestion strip should be updated after the operation of adding word to the
- // user dictionary
- mHandler.postUpdateSuggestions();
+ public boolean addWordToUserDictionary(String word) {
+ mUserDictionary.addWordToUserDictionary(word, 128);
return true;
}
@@ -1193,17 +1194,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private void performEditorAction(int actionId) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.performEditorAction(actionId);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_performEditorAction(actionId);
- }
- }
+ mConnection.performEditorAction(actionId);
}
private void handleLanguageSwitchKey() {
- final boolean includesOtherImes = mSettingsValues.mIncludesOtherImesInLanguageSwitchList;
+ final boolean includesOtherImes = mCurrentSettings.mIncludesOtherImesInLanguageSwitchList;
final IBinder token = getWindow().getWindow().getAttributes().token;
if (mShouldSwitchToLastSubtype) {
final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
@@ -1221,12 +1216,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- static private void sendUpDownEnterOrBackspace(final int code, final InputConnection ic) {
+ private void sendUpDownEnterOrBackspace(final int code) {
final long eventTime = SystemClock.uptimeMillis();
- ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
+ mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
- ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
}
@@ -1236,27 +1231,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
if (code >= '0' && code <= '9') {
super.sendKeyChar((char)code);
- return;
- }
-
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
- // we want to be able to compile against the Ice Cream Sandwich SDK.
- if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
- && mTargetApplicationInfo.targetSdkVersion < 16) {
- // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
- // a hardware keyboard event on pressing enter or delete. This is bad for many
- // reasons (there are race conditions with commits) but some applications are
- // relying on this behavior so we continue to support it for older apps.
- sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER, ic);
- } else {
- final String text = new String(new int[] { code }, 0, 1);
- ic.commitText(text, text.length());
- }
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.latinIME_sendKeyCodePoint(code);
}
+ return;
+ }
+
+ // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
+ // we want to be able to compile against the Ice Cream Sandwich SDK.
+ if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
+ && mTargetApplicationInfo.targetSdkVersion < 16) {
+ // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
+ // a hardware keyboard event on pressing enter or delete. This is bad for many
+ // reasons (there are race conditions with commits) but some applications are
+ // relying on this behavior so we continue to support it for older apps.
+ sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER);
+ } else {
+ final String text = new String(new int[] { code }, 0, 1);
+ mConnection.commitText(text, text.length());
}
}
@@ -1268,13 +1260,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mDeleteCount = 0;
}
mLastKeyTime = when;
-
- if (ProductionFlag.IS_EXPERIMENTAL) {
- if (ResearchLogger.sIsLogging) {
- ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y);
- }
- }
-
+ mConnection.beginBatchEdit();
final KeyboardSwitcher switcher = mKeyboardSwitcher;
// The space state depends only on the last character pressed and its own previous
// state. Here, we revert the space state to neutral if the key is actually modifying
@@ -1307,7 +1293,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
onSettingsKeyPressed();
break;
case Keyboard.CODE_SHORTCUT:
- mSubtypeSwitcher.switchToShortcutIME();
+ mSubtypeSwitcher.switchToShortcutIME(this);
break;
case Keyboard.CODE_ACTION_ENTER:
performEditorAction(getActionId(switcher.getKeyboard()));
@@ -1321,23 +1307,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
case Keyboard.CODE_LANGUAGE_SWITCH:
handleLanguageSwitchKey();
break;
- default:
- if (primaryCode == Keyboard.CODE_TAB
- && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
- performEditorAction(EditorInfo.IME_ACTION_NEXT);
- break;
+ case Keyboard.CODE_RESEARCH:
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().presentResearchDialog(this);
}
+ break;
+ default:
mSpaceState = SPACE_STATE_NONE;
- if (mSettingsValues.isWordSeparator(primaryCode)) {
+ if (mCurrentSettings.isWordSeparator(primaryCode)) {
didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
} else {
+ if (SPACE_STATE_PHANTOM == spaceState) {
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ }
+ final int keyX, keyY;
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
- handleCharacter(primaryCode, x, y, spaceState);
+ keyX = x;
+ keyY = y;
} else {
- handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
- spaceState);
+ keyX = Constants.NOT_A_COORDINATE;
+ keyY = Constants.NOT_A_COORDINATE;
}
+ handleCharacter(primaryCode, keyX, keyY, spaceState);
}
mExpectingUpdateSelection = true;
mShouldSwitchToLastSubtype = true;
@@ -1349,23 +1341,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
&& primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
mLastComposedWord.deactivate();
mEnteredText = null;
+ mConnection.endBatchEdit();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
+ }
}
+ // Called from PointerTracker through the KeyboardActionListener interface
@Override
- public void onTextInput(CharSequence text) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- ic.beginBatchEdit();
- commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
- text = specificTldProcessingOnTextInput(ic, text);
+ public void onTextInput(CharSequence rawText) {
+ mConnection.beginBatchEdit();
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ mHandler.postUpdateSuggestionStrip();
+ final CharSequence text = specificTldProcessingOnTextInput(rawText);
if (SPACE_STATE_PHANTOM == mSpaceState) {
sendKeyCodePoint(Keyboard.CODE_SPACE);
}
- ic.commitText(text, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(text);
- }
- ic.endBatchEdit();
+ mConnection.commitText(text, 1);
+ mConnection.endBatchEdit();
mKeyboardSwitcher.updateShiftState();
mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
mSpaceState = SPACE_STATE_NONE;
@@ -1373,9 +1366,61 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
resetComposingState(true /* alsoResetLastComposedWord */);
}
- // ic may not be null
- private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
- final CharSequence text) {
+ @Override
+ public void onStartBatchInput() {
+ mConnection.beginBatchEdit();
+ if (mWordComposer.isComposingWord()) {
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ mExpectingUpdateSelection = true;
+ // TODO: Can we remove this?
+ mSpaceState = SPACE_STATE_PHANTOM;
+ }
+ mConnection.endBatchEdit();
+ // TODO: Should handle TextUtils.CAP_MODE_CHARACTER.
+ mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+ }
+
+ @Override
+ public void onUpdateBatchInput(InputPointers batchPointers) {
+ mWordComposer.setBatchInputPointers(batchPointers);
+ final SuggestedWords suggestedWords = getSuggestedWords();
+ showSuggestionStrip(suggestedWords, null);
+ final String gestureFloatingPreviewText = (suggestedWords.size() > 0)
+ ? suggestedWords.getWord(0) : null;
+ mKeyboardSwitcher.getMainKeyboardView()
+ .showGestureFloatingPreviewText(gestureFloatingPreviewText);
+ }
+
+ @Override
+ public void onEndBatchInput(InputPointers batchPointers) {
+ mWordComposer.setBatchInputPointers(batchPointers);
+ final SuggestedWords suggestedWords = getSuggestedWords();
+ showSuggestionStrip(suggestedWords, null);
+ final String gestureFloatingPreviewText = (suggestedWords.size() > 0)
+ ? suggestedWords.getWord(0) : null;
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ mainKeyboardView.showGestureFloatingPreviewText(gestureFloatingPreviewText);
+ mainKeyboardView.dismissGestureFloatingPreviewText();
+ if (suggestedWords == null || suggestedWords.size() == 0) {
+ return;
+ }
+ final CharSequence text = suggestedWords.getWord(0);
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+ mWordComposer.setBatchInputWord(text);
+ mConnection.beginBatchEdit();
+ if (SPACE_STATE_PHANTOM == mSpaceState) {
+ sendKeyCodePoint(Keyboard.CODE_SPACE);
+ }
+ mConnection.setComposingText(text, 1);
+ mExpectingUpdateSelection = true;
+ mConnection.endBatchEdit();
+ mKeyboardSwitcher.updateShiftState();
+ mSpaceState = SPACE_STATE_PHANTOM;
+ }
+
+ private CharSequence specificTldProcessingOnTextInput(final CharSequence text) {
if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
|| !Character.isLetter(text.charAt(1))) {
// Not a tld: do nothing.
@@ -1384,7 +1429,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// We have a TLD (or something that looks like this): make sure we don't add
// a space even if currently in phantom mode.
mSpaceState = SPACE_STATE_NONE;
- final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
+ final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
if (lastOne != null && lastOne.length() == 1
&& lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
return text.subSequence(1, text.length());
@@ -1393,6 +1438,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
+ // Called from PointerTracker through the KeyboardActionListener interface
@Override
public void onCancelInput() {
// User released a finger outside any key
@@ -1400,27 +1446,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private void handleBackspace(final int spaceState) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- ic.beginBatchEdit();
- handleBackspaceWhileInBatchEdit(spaceState, ic);
- ic.endBatchEdit();
- }
-
- // "ic" may not be null.
- private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
// In many cases, we may have to put the keyboard in auto-shift state again.
mHandler.postUpdateShiftState();
- if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
+ if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
// Cancel multi-character input: remove the text we just entered.
// This is triggered on backspace after a key that inputs multiple characters,
// like the smiley key or the .com key.
final int length = mEnteredText.length();
- ic.deleteSurroundingText(length, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(length);
- }
+ mConnection.deleteSurroundingText(length, 0);
// If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
// In addition we know that spaceState is false, and that we should not be
// reverting any autocorrect at this point. So we can safely return.
@@ -1430,37 +1464,32 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (mWordComposer.isComposingWord()) {
final int length = mWordComposer.size();
if (length > 0) {
- mWordComposer.deleteLast();
- ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
- // If we have deleted the last remaining character of a word, then we are not
- // isComposingWord() any more.
- if (!mWordComposer.isComposingWord()) {
- // Not composing word any more, so we can show bigrams.
- mHandler.postUpdateBigramPredictions();
+ // Immediately after a batch input.
+ if (SPACE_STATE_PHANTOM == spaceState) {
+ mWordComposer.reset();
} else {
- // Still composing a word, so we still have letters to deduce a suggestion from.
- mHandler.postUpdateSuggestions();
+ mWordComposer.deleteLast();
}
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+ mHandler.postUpdateSuggestionStrip();
} else {
- ic.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
+ mConnection.deleteSurroundingText(1, 0);
}
} else {
if (mLastComposedWord.canRevertCommit()) {
Utils.Stats.onAutoCorrectionCancellation();
- revertCommit(ic);
+ revertCommit();
return;
}
if (SPACE_STATE_DOUBLE == spaceState) {
- if (revertDoubleSpaceWhileInBatchEdit(ic)) {
+ mHandler.cancelDoubleSpacesTimer();
+ if (mConnection.revertDoubleSpace()) {
// No need to reset mSpaceState, it has already be done (that's why we
// receive it as a parameter)
return;
}
} else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
- if (revertSwapPunctuation(ic)) {
+ if (mConnection.revertSwapPunctuation()) {
// Likewise
return;
}
@@ -1471,11 +1500,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (mLastSelectionStart != mLastSelectionEnd) {
// If there is a selection, remove it.
final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
- ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
- ic.deleteSurroundingText(lengthToDelete, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
- }
+ mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+ mConnection.deleteSurroundingText(lengthToDelete, 0);
} else {
// There is no selection, just delete one character.
if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
@@ -1490,40 +1516,33 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// a hardware keyboard event on pressing enter or delete. This is bad for many
// reasons (there are race conditions with commits) but some applications are
// relying on this behavior so we continue to support it for older apps.
- sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL, ic);
+ sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL);
} else {
- ic.deleteSurroundingText(1, 0);
- }
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
+ mConnection.deleteSurroundingText(1, 0);
}
if (mDeleteCount > DELETE_ACCELERATE_AT) {
- ic.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
+ mConnection.deleteSurroundingText(1, 0);
}
}
- if (isSuggestionsRequested()) {
- restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+ restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
}
}
}
- // ic may be null
- private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
+ private boolean maybeStripSpace(final int code,
final int spaceState, final boolean isFromSuggestionStrip) {
if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
- removeTrailingSpaceWhileInBatchEdit(ic);
+ mConnection.removeTrailingSpace();
return false;
} else if ((SPACE_STATE_WEAK == spaceState
|| SPACE_STATE_SWAP_PUNCTUATION == spaceState)
&& isFromSuggestionStrip) {
- if (mSettingsValues.isWeakSpaceSwapper(code)) {
+ if (mCurrentSettings.isWeakSpaceSwapper(code)) {
return true;
} else {
- if (mSettingsValues.isWeakSpaceStripper(code)) {
- removeTrailingSpaceWhileInBatchEdit(ic);
+ if (mCurrentSettings.isWeakSpaceStripper(code)) {
+ mConnection.removeTrailingSpace();
}
return false;
}
@@ -1534,20 +1553,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void handleCharacter(final int primaryCode, final int x,
final int y, final int spaceState) {
- final InputConnection ic = getCurrentInputConnection();
- if (null != ic) ic.beginBatchEdit();
- // TODO: if ic is null, does it make any sense to call this?
- handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
- if (null != ic) ic.endBatchEdit();
- }
-
- // "ic" may be null without this crashing, but the behavior will be really strange
- private void handleCharacterWhileInBatchEdit(final int primaryCode,
- final int x, final int y, final int spaceState, final InputConnection ic) {
boolean isComposingWord = mWordComposer.isComposingWord();
if (SPACE_STATE_PHANTOM == spaceState &&
- !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
+ !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) {
if (isComposingWord) {
// Sanity check
throw new RuntimeException("Should not be composing here");
@@ -1559,8 +1568,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
// thread here.
if (!isComposingWord && (isAlphabet(primaryCode)
- || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
- && isSuggestionsRequested() && !isCursorTouchingWord()) {
+ || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode))
+ && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) &&
+ !mConnection.isCursorTouchingWord(mCurrentSettings)) {
// Reset entirely the composing state anyway, then start composing a new word unless
// the character is a single quote. The idea here is, single quote is not a
// separator and it should be treated as a normal character, except in the first
@@ -1571,85 +1581,68 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// it entirely and resume suggestions on the previous word, we'd like to still
// have touch coordinates for it.
resetComposingState(false /* alsoResetLastComposedWord */);
- clearSuggestions();
}
if (isComposingWord) {
- mWordComposer.add(
- primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
- if (ic != null) {
- // If it's the first letter, make note of auto-caps state
- if (mWordComposer.size() == 1) {
- mWordComposer.setAutoCapitalized(
- getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
- }
- ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+ final int keyX, keyY;
+ if (KeyboardActionListener.Adapter.isInvalidCoordinate(x)
+ || KeyboardActionListener.Adapter.isInvalidCoordinate(y)) {
+ keyX = x;
+ keyY = y;
+ } else {
+ final KeyDetector keyDetector =
+ mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
+ keyX = keyDetector.getTouchX(x);
+ keyY = keyDetector.getTouchY(y);
+ }
+ mWordComposer.add(primaryCode, keyX, keyY);
+ // If it's the first letter, make note of auto-caps state
+ if (mWordComposer.size() == 1) {
+ mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
}
- mHandler.postUpdateSuggestions();
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
} else {
- final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
- spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+ final boolean swapWeakSpace = maybeStripSpace(primaryCode,
+ spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
sendKeyCodePoint(primaryCode);
if (swapWeakSpace) {
- swapSwapperAndSpaceWhileInBatchEdit(ic);
+ swapSwapperAndSpace();
mSpaceState = SPACE_STATE_WEAK;
}
- // Some characters are not word separators, yet they don't start a new
- // composing span. For these, we haven't changed the suggestion strip, and
- // if the "add to dictionary" hint is shown, we should do so now. Examples of
- // such characters include single quote, dollar, and others; the exact list is
- // the list of characters for which we enter handleCharacterWhileInBatchEdit
- // that don't match the test if ((isAlphabet...)) at the top of this method.
- if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
- mHandler.postUpdateBigramPredictions();
- }
+ // In case the "add to dictionary" hint was still displayed.
+ if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
}
+ mHandler.postUpdateSuggestionStrip();
Utils.Stats.onNonSeparator((char)primaryCode, x, y);
}
// Returns true if we did an autocorrection, false otherwise.
private boolean handleSeparator(final int primaryCode, final int x, final int y,
final int spaceState) {
- // Should dismiss the "Touch again to save" message when handling separator
- if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
- mHandler.cancelUpdateBigramPredictions();
- mHandler.postUpdateSuggestions();
- }
-
boolean didAutoCorrect = false;
// Handle separator
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.beginBatchEdit();
- }
if (mWordComposer.isComposingWord()) {
- // In certain languages where single quote is a separator, it's better
- // not to auto correct, but accept the typed word. For instance,
- // in Italian dov' should not be expanded to dove' because the elision
- // requires the last vowel to be removed.
- final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
- && !mInputAttributes.mInputTypeNoAutoCorrect;
- if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
- commitCurrentAutoCorrection(primaryCode, ic);
+ if (mCurrentSettings.mCorrectionEnabled) {
+ commitCurrentAutoCorrection(primaryCode);
didAutoCorrect = true;
} else {
- commitTyped(ic, primaryCode);
+ commitTyped(primaryCode);
}
}
- final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
- KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+ final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
+ Constants.SUGGESTION_STRIP_COORDINATE == x);
if (SPACE_STATE_PHANTOM == spaceState &&
- mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
+ mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
sendKeyCodePoint(Keyboard.CODE_SPACE);
}
sendKeyCodePoint(primaryCode);
if (Keyboard.CODE_SPACE == primaryCode) {
- if (isSuggestionsRequested()) {
- if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+ if (maybeDoubleSpace()) {
mSpaceState = SPACE_STATE_DOUBLE;
} else if (!isShowingPunctuationList()) {
mSpaceState = SPACE_STATE_WEAK;
@@ -1657,21 +1650,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mHandler.startDoubleSpacesTimer();
- if (!isCursorTouchingWord()) {
- mHandler.cancelUpdateSuggestions();
- mHandler.postUpdateBigramPredictions();
+ if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
+ mHandler.postUpdateSuggestionStrip();
}
} else {
if (swapWeakSpace) {
- swapSwapperAndSpaceWhileInBatchEdit(ic);
+ swapSwapperAndSpace();
mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
- } else if (SPACE_STATE_PHANTOM == spaceState) {
+ } else if (SPACE_STATE_PHANTOM == spaceState
+ && !mCurrentSettings.isWeakSpaceStripper(primaryCode)) {
// If we are in phantom space state, and the user presses a separator, we want to
// stay in phantom space state so that the next keypress has a chance to add the
// space. For example, if I type "Good dat", pick "day" from the suggestion strip
// then insert a comma and go on to typing the next word, I want the space to be
// inserted automatically before the next word, the same way it is when I don't
// input the comma.
+ // The case is a little different if the separator is a space stripper. Such a
+ // separator does not normally need a space on the right (that's the difference
+ // between swappers and strippers), so we should not stay in phantom space state if
+ // the separator is a stripper. Hence the additional test above.
mSpaceState = SPACE_STATE_PHANTOM;
}
@@ -1682,9 +1679,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
Utils.Stats.onSeparator((char)primaryCode, x, y);
- if (ic != null) {
- ic.endBatchEdit();
- }
+ mHandler.postUpdateShiftState();
return didAutoCorrect;
}
@@ -1695,153 +1690,134 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private void handleClose() {
- commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
requestHideSelf(0);
- LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null)
- inputView.closing();
- }
-
- public boolean isSuggestionsRequested() {
- return mInputAttributes.mIsSettingsSuggestionStripOn
- && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
- }
-
- public boolean isShowingPunctuationList() {
- if (mSuggestionsView == null) return false;
- return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.closing();
+ }
}
- public boolean isShowingSuggestionsStrip() {
- return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
- || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
- && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
+ // TODO: make this private
+ // Outside LatinIME, only used by the test suite.
+ /* package for tests */
+ boolean isShowingPunctuationList() {
+ if (mSuggestionStripView == null) return false;
+ return mCurrentSettings.mSuggestPuncList == mSuggestionStripView.getSuggestions();
}
- public boolean isSuggestionsStripVisible() {
- if (mSuggestionsView == null)
+ private boolean isSuggestionsStripVisible() {
+ if (mSuggestionStripView == null)
return false;
- if (mSuggestionsView.isShowingAddToDictionaryHint())
+ if (mSuggestionStripView.isShowingAddToDictionaryHint())
return true;
- if (!isShowingSuggestionsStrip())
+ if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
return false;
- if (mInputAttributes.mApplicationSpecifiedCompletionOn)
+ if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
return true;
- return isSuggestionsRequested();
+ return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation);
}
- public void switchToKeyboardView() {
- if (DEBUG) {
- Log.d(TAG, "Switch to keyboard view.");
- }
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_switchToKeyboardView();
- }
- View v = mKeyboardSwitcher.getKeyboardView();
- if (v != null) {
- // Confirms that the keyboard view doesn't have parent view.
- ViewParent p = v.getParent();
- if (p != null && p instanceof ViewGroup) {
- ((ViewGroup) p).removeView(v);
- }
- setInputView(v);
- }
- setSuggestionStripShown(isSuggestionsStripVisible());
- updateInputViewShown();
- mHandler.postUpdateSuggestions();
- }
-
- public void clearSuggestions() {
- setSuggestions(SuggestedWords.EMPTY, false);
+ private void clearSuggestionStrip() {
+ setSuggestionStrip(SuggestedWords.EMPTY, false);
setAutoCorrectionIndicator(false);
}
- private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
- if (mSuggestionsView != null) {
- mSuggestionsView.setSuggestions(words);
+ private void setSuggestionStrip(final SuggestedWords words, final boolean isAutoCorrection) {
+ if (mSuggestionStripView != null) {
+ mSuggestionStripView.setSuggestions(words);
mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
}
}
private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
// Put a blue underline to a word in TextView which will be auto-corrected.
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
&& mWordComposer.isComposingWord()) {
mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
final CharSequence textWithUnderline =
getTextWithUnderline(mWordComposer.getTypedWord());
- ic.setComposingText(textWithUnderline, 1);
+ mConnection.setComposingText(textWithUnderline, 1);
}
}
- public void updateSuggestions() {
+ private void updateSuggestionStrip() {
+ mHandler.cancelUpdateSuggestionStrip();
+
// Check if we have a suggestion engine attached.
- if ((mSuggest == null || !isSuggestionsRequested())) {
+ if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
if (mWordComposer.isComposingWord()) {
- Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
+ Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
+ + "requested!");
mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
}
return;
}
- mHandler.cancelUpdateSuggestions();
- mHandler.cancelUpdateBigramPredictions();
-
- if (!mWordComposer.isComposingWord()) {
+ if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) {
setPunctuationSuggestions();
return;
}
- // TODO: May need a better way of retrieving previous word
- final InputConnection ic = getCurrentInputConnection();
- final CharSequence prevWord;
- if (null == ic) {
- prevWord = null;
- } else {
- prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
- }
+ final SuggestedWords suggestedWords = getSuggestedWords();
+ final String typedWord = mWordComposer.getTypedWord();
+ showSuggestionStrip(suggestedWords, typedWord);
+ }
- final CharSequence typedWord = mWordComposer.getTypedWord();
- // getSuggestedWords handles gracefully a null value of prevWord
+ private SuggestedWords getSuggestedWords() {
+ final String typedWord = mWordComposer.getTypedWord();
+ // Get the word on which we should search the bigrams. If we are composing a word, it's
+ // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
+ // should just skip whitespace if any, so 1.
+ // TODO: this is slow (2-way IPC) - we should probably cache this instead.
+ final CharSequence prevWord =
+ mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators,
+ mWordComposer.isComposingWord() ? 2 : 1);
final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
- prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
-
- // Basically, we update the suggestion strip only when suggestion count > 1. However,
- // there is an exception: We update the suggestion strip whenever typed word's length
- // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually,
- // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
- // need to clear the previous state when the user starts typing a word (i.e. typed word's
- // length == 1).
- if (suggestedWords.size() > 1 || typedWord.length() == 1
- || !suggestedWords.mAllowsToBeAutoCorrected
- || mSuggestionsView.isShowingAddToDictionaryHint()) {
- showSuggestions(suggestedWords, typedWord);
+ prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
+ mCurrentSettings.mCorrectionEnabled);
+ return maybeRetrieveOlderSuggestions(typedWord, suggestedWords);
+ }
+
+ private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord,
+ final SuggestedWords suggestedWords) {
+ // TODO: consolidate this into getSuggestedWords
+ // We update the suggestion strip only when we have some suggestions to show, i.e. when
+ // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
+ // replaced with the new one. However, when the word is a dictionary word, or when the
+ // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
+ // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
+ // revert to suggestions - although it is unclear how we can come here if it's displayed.
+ if (suggestedWords.size() > 1 || typedWord.length() <= 1
+ || !suggestedWords.mTypedWordValid
+ || mSuggestionStripView.isShowingAddToDictionaryHint()) {
+ return suggestedWords;
} else {
- SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
- if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
+ SuggestedWords previousSuggestions = mSuggestionStripView.getSuggestions();
+ if (previousSuggestions == mCurrentSettings.mSuggestPuncList) {
previousSuggestions = SuggestedWords.EMPTY;
}
final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
SuggestedWords.getTypedWordAndPreviousSuggestions(
typedWord, previousSuggestions);
- final SuggestedWords obsoleteSuggestedWords =
- new SuggestedWords(typedWordAndPreviousSuggestions,
+ return new SuggestedWords(typedWordAndPreviousSuggestions,
false /* typedWordValid */,
false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
false /* isPunctuationSuggestions */,
true /* isObsoleteSuggestions */,
false /* isPrediction */);
- showSuggestions(obsoleteSuggestedWords, typedWord);
}
}
- public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
+ private void showSuggestionStrip(final SuggestedWords suggestedWords,
+ final CharSequence typedWord) {
+ if (null == suggestedWords || suggestedWords.size() <= 0) {
+ clearSuggestionStrip();
+ return;
+ }
final CharSequence autoCorrection;
if (suggestedWords.size() > 0) {
- if (suggestedWords.hasAutoCorrectionWord()) {
+ if (suggestedWords.mWillAutoCorrect) {
autoCorrection = suggestedWords.getWord(1);
} else {
autoCorrection = typedWord;
@@ -1851,94 +1827,82 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mWordComposer.setAutoCorrection(autoCorrection);
final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
- setSuggestions(suggestedWords, isAutoCorrection);
+ setSuggestionStrip(suggestedWords, isAutoCorrection);
setAutoCorrectionIndicator(isAutoCorrection);
setSuggestionStripShown(isSuggestionsStripVisible());
}
- private void commitCurrentAutoCorrection(final int separatorCodePoint,
- final InputConnection ic) {
+ private void commitCurrentAutoCorrection(final int separatorCodePoint) {
// Complete any pending suggestions query first
if (mHandler.hasPendingUpdateSuggestions()) {
- mHandler.cancelUpdateSuggestions();
- updateSuggestions();
+ updateSuggestionStrip();
}
- final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
+ final CharSequence typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
+ final String typedWord = mWordComposer.getTypedWord();
+ final CharSequence autoCorrection = (typedAutoCorrection != null)
+ ? typedAutoCorrection : typedWord;
if (autoCorrection != null) {
- final String typedWord = mWordComposer.getTypedWord();
if (TextUtils.isEmpty(typedWord)) {
throw new RuntimeException("We have an auto-correction but the typed word "
+ "is empty? Impossible! I must commit suicide.");
}
Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
- autoCorrection.toString());
- }
mExpectingUpdateSelection = true;
commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
separatorCodePoint);
- if (!typedWord.equals(autoCorrection) && null != ic) {
+ if (!typedWord.equals(autoCorrection)) {
// This will make the correction flash for a short while as a visual clue
// to the user that auto-correction happened.
- ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+ mConnection.commitCorrection(
+ new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
typedWord, autoCorrection));
}
}
}
+ // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
+ // interface
@Override
- public void pickSuggestionManually(final int index, final CharSequence suggestion,
- int x, int y) {
- final InputConnection ic = getCurrentInputConnection();
- if (null != ic) ic.beginBatchEdit();
- pickSuggestionManuallyWhileInBatchEdit(index, suggestion, x, y, ic);
- if (null != ic) ic.endBatchEdit();
- }
-
- public void pickSuggestionManuallyWhileInBatchEdit(final int index,
- final CharSequence suggestion, final int x, final int y, final InputConnection ic) {
- final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
+ public void pickSuggestionManually(final int index, final CharSequence suggestion) {
+ final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions();
// If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
if (suggestion.length() == 1 && isShowingPunctuationList()) {
// Word separators are suggested before the user inputs something.
// So, LatinImeLogger logs "" as a user's input.
LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
// Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
- }
final int primaryCode = suggestion.charAt(0);
onCodeInput(primaryCode,
- KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
- KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
+ Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_punctuationSuggestion(index, suggestion);
+ }
return;
}
- if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
+ mConnection.beginBatchEdit();
+ if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
+ // In the batch input mode, a manually picked suggested word should just replace
+ // the current batch input text and there is no need for a phantom space.
+ && !mWordComposer.isBatchMode()) {
int firstChar = Character.codePointAt(suggestion, 0);
- if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
- && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
+ if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
+ && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
sendKeyCodePoint(Keyboard.CODE_SPACE);
}
}
- if (mInputAttributes.mApplicationSpecifiedCompletionOn
+ if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
&& mApplicationSpecifiedCompletions != null
&& index >= 0 && index < mApplicationSpecifiedCompletions.length) {
- if (mSuggestionsView != null) {
- mSuggestionsView.clear();
+ if (mSuggestionStripView != null) {
+ mSuggestionStripView.clear();
}
mKeyboardSwitcher.updateShiftState();
resetComposingState(true /* alsoResetLastComposedWord */);
- if (ic != null) {
- final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
- ic.commitCompletion(completionInfo);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
- completionInfo.getText(), x, y);
- }
- }
+ final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
+ mConnection.commitCompletion(completionInfo);
+ mConnection.endBatchEdit();
return;
}
@@ -1947,12 +1911,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final String replacedWord = mWordComposer.getTypedWord().toString();
LatinImeLogger.logOnManualSuggestion(replacedWord,
suggestion.toString(), index, suggestedWords);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
- }
mExpectingUpdateSelection = true;
commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
LastComposedWord.NOT_A_SEPARATOR);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion);
+ }
+ mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
mLastComposedWord.deactivate();
mSpaceState = SPACE_STATE_PHANTOM;
@@ -1960,37 +1925,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mKeyboardSwitcher.updateShiftState();
// We should show the "Touch again to save" hint if the user pressed the first entry
- // AND either:
- // - There is no dictionary (we know that because we tried to load it => null != mSuggest
- // AND mSuggest.hasMainDictionary() is false)
- // - There is a dictionary and the word is not in it
+ // AND it's in none of our current dictionaries (main, user or otherwise).
// Please note that if mSuggest is null, it means that everything is off: suggestion
// and correction, so we shouldn't try to show the hint
- // We used to look at mCorrectionMode here, but showing the hint should have nothing
- // to do with the autocorrection setting.
final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
- // If there is no dictionary the hint should be shown.
- && (!mSuggest.hasMainDictionary()
- // If "suggestion" is not in the dictionary, the hint should be shown.
- || !AutoCorrection.isValidWord(
- mSuggest.getUnigramDictionaries(), suggestion, true));
-
- Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
- WordComposer.NOT_A_COORDINATE);
- if (!showingAddToDictionaryHint) {
- // If we're not showing the "Touch again to save", then show corrections again.
- // In case the cursor position doesn't change, make sure we show the suggestions again.
- updateBigramPredictions();
- // Updating the predictions right away may be slow and feel unresponsive on slower
- // terminals. On the other hand if we just postUpdateBigramPredictions() it will
- // take a noticeable delay to update them which may feel uneasy.
+ // If the suggestion is not in the dictionary, the hint should be shown.
+ && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true);
+
+ Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
+ mSuggestionStripView.showAddToDictionaryHint(
+ suggestion, mCurrentSettings.mHintToSaveText);
} else {
- if (mIsUserDictionaryAvailable) {
- mSuggestionsView.showAddToDictionaryHint(
- suggestion, mSettingsValues.mHintToSaveText);
- } else {
- mHandler.postUpdateSuggestions();
- }
+ // If we're not showing the "Touch again to save", then update the suggestion strip.
+ mHandler.postUpdateSuggestionStrip();
}
}
@@ -1999,23 +1948,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
private void commitChosenWord(final CharSequence chosenWord, final int commitType,
final int separatorCode) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- if (mSettingsValues.mEnableSuggestionSpanInsertion) {
- final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
- ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
- this, chosenWord, suggestedWords, mIsMainDictionaryAvailable),
- 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(chosenWord);
- }
- } else {
- ic.commitText(chosenWord, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(chosenWord);
- }
- }
- }
+ final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions();
+ mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
+ this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
// Add the word to the user history dictionary
final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
// TODO: figure out here if this is an auto-correct or if the best word is actually
@@ -2026,42 +1961,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
separatorCode, prevWord);
}
- public void updateBigramPredictions() {
- if (mSuggest == null || !isSuggestionsRequested())
- return;
-
- if (!mSettingsValues.mBigramPredictionEnabled) {
- setPunctuationSuggestions();
- return;
- }
-
- final SuggestedWords suggestedWords;
- if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
- final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
- mSettingsValues.mWordSeparators);
- if (!TextUtils.isEmpty(prevWord)) {
- suggestedWords = mSuggest.getBigramPredictions(prevWord);
- } else {
- suggestedWords = null;
- }
- } else {
- suggestedWords = null;
- }
-
- if (null != suggestedWords && suggestedWords.size() > 0) {
- // Explicitly supply an empty typed word (the no-second-arg version of
- // showSuggestions will retrieve the word near the cursor, we don't want that here)
- showSuggestions(suggestedWords, "");
- } else {
- clearSuggestions();
- }
- }
-
- public void setPunctuationSuggestions() {
- if (mSettingsValues.mBigramPredictionEnabled) {
- clearSuggestions();
+ private void setPunctuationSuggestions() {
+ if (mCurrentSettings.mBigramPredictionEnabled) {
+ clearSuggestionStrip();
} else {
- setSuggestions(mSettingsValues.mSuggestPuncList, false);
+ setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
}
setAutoCorrectionIndicator(false);
setSuggestionStripShown(isSuggestionsStripVisible());
@@ -2069,25 +1973,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
if (TextUtils.isEmpty(suggestion)) return null;
+ if (mSuggest == null) return null;
- // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
- // adding words in situations where the user or application really didn't
- // want corrections enabled or learned.
- if (!(mCorrectionMode == Suggest.CORRECTION_FULL
- || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
- return null;
- }
+ // If correction is not enabled, we don't add words to the user history dictionary.
+ // That's to avoid unintended additions in some sensitive fields, or fields that
+ // expect to receive non-words.
+ if (!mCurrentSettings.mCorrectionEnabled) return null;
- if (mUserHistoryDictionary != null) {
- final InputConnection ic = getCurrentInputConnection();
- final CharSequence prevWord;
- if (null != ic) {
- prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
- } else {
- prevWord = null;
- }
+ final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
+ if (userHistoryDictionary != null) {
+ final CharSequence prevWord
+ = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2);
final String secondWord;
- if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
+ if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
secondWord = suggestion.toString().toLowerCase(
mSubtypeSwitcher.getCurrentSubtypeLocale());
} else {
@@ -2098,95 +1996,33 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final int maxFreq = AutoCorrection.getMaxFrequency(
mSuggest.getUnigramDictionaries(), suggestion);
if (maxFreq == 0) return null;
- mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
+ userHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
secondWord, maxFreq > 0);
return prevWord;
}
return null;
}
- public boolean isCursorTouchingWord() {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return false;
- CharSequence before = ic.getTextBeforeCursor(1, 0);
- CharSequence after = ic.getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))
- && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
- return true;
- }
- if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))
- && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
- return true;
- }
- return false;
- }
-
- // "ic" must not be null
- private static boolean sameAsTextBeforeCursor(final InputConnection ic,
- final CharSequence text) {
- final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
- return TextUtils.equals(text, beforeText);
- }
-
- // "ic" must not be null
/**
* Check if the cursor is actually at the end of a word. If so, restart suggestions on this
* word, else do nothing.
*/
- private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
- final InputConnection ic) {
- // Bail out if the cursor is not at the end of a word (cursor must be preceded by
- // non-whitespace, non-separator, non-start-of-text)
- // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
- final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
- if (TextUtils.isEmpty(textBeforeCursor)
- || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
-
- // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
- // separator or end of line/text)
- // Example: "test|"<EOL> "te|st" get rejected here
- final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(textAfterCursor)
- && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
-
- // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
- // Example: " -|" gets rejected here but "e-|" and "e|" are okay
- CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
- // We don't suggest on leading single quotes, so we have to remove them from the word if
- // it starts with single quotes.
- while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
- word = word.subSequence(1, word.length());
- }
- if (TextUtils.isEmpty(word)) return;
- final char firstChar = word.charAt(0); // we just tested that word is not empty
- if (word.length() == 1 && !Character.isLetter(firstChar)) return;
-
- // We only suggest on words that start with a letter or a symbol that is excluded from
- // word separators (see #handleCharacterWhileInBatchEdit).
- if (!(isAlphabet(firstChar)
- || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
- return;
+ private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
+ final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings);
+ if (null != word) {
+ restartSuggestionsOnWordBeforeCursor(word);
}
-
- // Okay, we are at the end of a word. Restart suggestions.
- restartSuggestionsOnWordBeforeCursor(ic, word);
}
- // "ic" must not be null
- private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
- final CharSequence word) {
+ private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
final int length = word.length();
- ic.deleteSurroundingText(length, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(length);
- }
- ic.setComposingText(word, 1);
- mHandler.postUpdateSuggestions();
+ mConnection.deleteSurroundingText(length, 0);
+ mConnection.setComposingText(word, 1);
+ mHandler.postUpdateSuggestionStrip();
}
- // "ic" must not be null
- private void revertCommit(final InputConnection ic) {
+ private void revertCommit() {
final CharSequence previousWord = mLastComposedWord.mPrevWord;
final String originallyTypedWord = mLastComposedWord.mTypedWord;
final CharSequence committedWord = mLastComposedWord.mCommittedWord;
@@ -2200,7 +2036,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
throw new RuntimeException("revertCommit, but we are composing a word");
}
final String wordBeforeCursor =
- ic.getTextBeforeCursor(deleteLength, 0)
+ mConnection.getTextBeforeCursor(deleteLength, 0)
.subSequence(0, cancelLength).toString();
if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
throw new RuntimeException("revertCommit check failed: we thought we were "
@@ -2208,130 +2044,65 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
+ "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
}
}
- ic.deleteSurroundingText(deleteLength, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
- }
+ mConnection.deleteSurroundingText(deleteLength, 0);
if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
mUserHistoryDictionary.cancelAddingUserHistory(
previousWord.toString(), committedWord.toString());
}
- if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
- // This is the case when we cancel a manual pick.
- // We should restart suggestion on the word right away.
- mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
- ic.setComposingText(originallyTypedWord, 1);
- } else {
- ic.commitText(originallyTypedWord, 1);
- // Re-insert the separator
- sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
- Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
- WordComposer.NOT_A_COORDINATE);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertCommit(originallyTypedWord);
- }
- // Don't restart suggestion yet. We'll restart if the user deletes the
- // separator.
- }
- mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
- mHandler.cancelUpdateBigramPredictions();
- mHandler.postUpdateSuggestions();
- }
-
- // "ic" must not be null
- private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
- mHandler.cancelDoubleSpacesTimer();
- // Here we test whether we indeed have a period and a space before us. This should not
- // be needed, but it's there just in case something went wrong.
- final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
- if (!". ".equals(textBeforeCursor)) {
- // Theoretically we should not be coming here if there isn't ". " before the
- // cursor, but the application may be changing the text while we are typing, so
- // anything goes. We should not crash.
- Log.d(TAG, "Tried to revert double-space combo but we didn't find "
- + "\". \" just before the cursor.");
- return false;
- }
- ic.deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
- ic.commitText(" ", 1);
+ mConnection.commitText(originallyTypedWord, 1);
+ // Re-insert the separator
+ sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
+ Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
+ ResearchLogger.latinIME_revertCommit(originallyTypedWord);
}
- return true;
- }
-
- private static boolean revertSwapPunctuation(final InputConnection ic) {
- // Here we test whether we indeed have a space and something else before us. This should not
- // be needed, but it's there just in case something went wrong.
- final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
- // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
- // enter surrogate pairs this code will have been removed.
- if (TextUtils.isEmpty(textBeforeCursor)
- || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
- // We may only come here if the application is changing the text while we are typing.
- // This is quite a broken case, but not logically impossible, so we shouldn't crash,
- // but some debugging log may be in order.
- Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
- + "find a space just before the cursor.");
- return false;
- }
- ic.beginBatchEdit();
- ic.deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
- ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertSwapPunctuation();
- }
- ic.endBatchEdit();
- return true;
+ // Don't restart suggestion yet. We'll restart if the user deletes the
+ // separator.
+ mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ // We have a separator between the word and the cursor: we should show predictions.
+ mHandler.postUpdateSuggestionStrip();
}
+ // Used by the RingCharBuffer
public boolean isWordSeparator(int code) {
- return mSettingsValues.isWordSeparator(code);
- }
-
- public boolean preferCapitalization() {
- return mWordComposer.isFirstCharCapitalized();
+ return mCurrentSettings.isWordSeparator(code);
}
- // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
- // according to new language or mode.
- public void onRefreshKeyboard() {
+ // TODO: Make this private
+ // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
+ /* package for test */
+ void loadKeyboard() {
// When the device locale is changed in SetupWizard etc., this method may get called via
// onConfigurationChanged before SoftInputWindow is shown.
- if (mKeyboardSwitcher.getKeyboardView() != null) {
- // Reload keyboard because the current language has been changed.
- mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
- }
initSuggest();
- updateCorrectionMode();
loadSettings();
+ if (mKeyboardSwitcher.getMainKeyboardView() != null) {
+ // Reload keyboard because the current language has been changed.
+ mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
+ }
// Since we just changed languages, we should re-evaluate suggestions with whatever word
// we are currently composing. If we are not composing anything, we may want to display
- // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
- if (isCursorTouchingWord()) {
- mHandler.postUpdateSuggestions();
- } else {
- mHandler.postUpdateBigramPredictions();
- }
+ // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway).
+ mHandler.postUpdateSuggestionStrip();
}
// TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
- // {@link KeyboardSwitcher}.
+ // {@link KeyboardSwitcher}. Called from KeyboardSwitcher
public void hapticAndAudioFeedback(final int primaryCode) {
- mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
+ mFeedbackManager.hapticAndAudioFeedback(
+ primaryCode, mKeyboardSwitcher.getMainKeyboardView());
}
+ // Callback called by PointerTracker through the KeyboardActionListener. This is called when a
+ // key is depressed; release matching call is onReleaseKey below.
@Override
public void onPressKey(int primaryCode) {
mKeyboardSwitcher.onPressKey(primaryCode);
}
+ // Callback by PointerTracker through the KeyboardActionListener. This is called when a key
+ // is released; press matching call is onPressKey above.
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {
mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
@@ -2352,12 +2123,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
// In the future, we need to deprecate deteleSurroundingText() and have a surrogate
// pair-friendly way of deleting characters in InputConnection.
- final InputConnection ic = getCurrentInputConnection();
- if (null != ic) {
- final CharSequence lastChar = ic.getTextBeforeCursor(1, 0);
- if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
- ic.deleteSurroundingText(1, 0);
- }
+ final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
+ if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
+ mConnection.deleteSurroundingText(1, 0);
}
}
}
@@ -2375,37 +2143,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
};
- private void updateCorrectionMode() {
- // TODO: cleanup messy flags
- final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
- && !mInputAttributes.mInputTypeNoAutoCorrect;
- mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
- mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
- ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
- }
-
- private void updateSuggestionVisibility(final Resources res) {
- final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
- for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
- if (suggestionVisiblityStr.equals(res.getString(visibility))) {
- mSuggestionVisibility = visibility;
- break;
- }
- }
- }
-
private void launchSettings() {
- launchSettingsClass(SettingsActivity.class);
+ handleClose();
+ launchSubActivity(SettingsActivity.class);
}
+ // Called from debug code only
public void launchDebugSettings() {
- launchSettingsClass(DebugSettingsActivity.class);
+ handleClose();
+ launchSubActivity(DebugSettingsActivity.class);
}
- private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
- handleClose();
+ public void launchKeyboardedDialogActivity(Class<? extends Activity> activityClass) {
+ // Put the text in the attached EditText into a safe, saved state before switching to a
+ // new activity that will also use the soft keyboard.
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ launchSubActivity(activityClass);
+ }
+
+ private void launchSubActivity(Class<? extends Activity> activityClass) {
Intent intent = new Intent();
- intent.setClass(LatinIME.this, settingsClass);
+ intent.setClass(LatinIME.this, activityClass);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
@@ -2440,12 +2198,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setItems(items, listener)
.setTitle(title);
- showOptionDialogInternal(builder.create());
+ showOptionDialog(builder.create());
}
- private void showOptionDialogInternal(AlertDialog dialog) {
- final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
- if (windowToken == null) return;
+ public void showOptionDialog(AlertDialog dialog) {
+ final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
+ if (windowToken == null) {
+ return;
+ }
dialog.setCancelable(true);
dialog.setCanceledOnTouchOutside(true);
@@ -2470,13 +2230,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
p.println(" Keyboard mode = " + keyboardMode);
- p.println(" mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
- p.println(" mCorrectionMode=" + mCorrectionMode);
+ p.println(" mIsSuggestionsSuggestionsRequested = "
+ + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation));
+ p.println(" mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled);
p.println(" isComposingWord=" + mWordComposer.isComposingWord());
- p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
- p.println(" mSoundOn=" + mSettingsValues.mSoundOn);
- p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn);
- p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
- p.println(" mInputAttributes=" + mInputAttributes.toString());
+ p.println(" mSoundOn=" + mCurrentSettings.mSoundOn);
+ p.println(" mVibrateOn=" + mCurrentSettings.mVibrateOn);
+ p.println(" mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
+ p.println(" inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
}
}
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index dc0868e7c..e843848bc 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -71,7 +71,7 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
public static void onStartSuggestion(CharSequence previousWords) {
}
- public static void onAddSuggestedWord(String word, int typeId, int dataType) {
+ public static void onAddSuggestedWord(String word, String sourceDictionaryId) {
}
public static void onSetKeyboard(Keyboard kb) {
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java
index b938dd336..3b08cab01 100644
--- a/java/src/com/android/inputmethod/latin/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java
@@ -193,7 +193,7 @@ public class LocaleUtils {
}
}
- private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>();
+ private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap();
/**
* Creates a locale from a string specification.
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
deleted file mode 100644
index 66d6d58b1..000000000
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ /dev/null
@@ -1,757 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * 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.android.inputmethod.latin;
-
-import android.content.SharedPreferences;
-import android.inputmethodservice.InputMethodService;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.os.SystemClock;
-import android.preference.PreferenceManager;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.inputmethod.CompletionInfo;
-import android.view.inputmethod.EditorInfo;
-
-import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.internal.KeyboardState;
-import com.android.inputmethod.latin.define.ProductionFlag;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.charset.Charset;
-import java.util.Map;
-
-/**
- * Logs the use of the LatinIME keyboard.
- *
- * This class logs operations on the IME keyboard, including what the user has typed.
- * Data is stored locally in a file in app-specific storage.
- *
- * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
- */
-public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = ResearchLogger.class.getSimpleName();
- private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
- private static final boolean DEBUG = false;
-
- private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager());
- public static boolean sIsLogging = false;
- /* package */ final Handler mLoggingHandler;
- private InputMethodService mIms;
-
- /**
- * Isolates management of files. This variable should never be null, but can be changed
- * to support testing.
- */
- /* package */ LogFileManager mLogFileManager;
-
- /**
- * Manages the file(s) that stores the logs.
- *
- * Handles creation, deletion, and provides Readers, Writers, and InputStreams to access
- * the logs.
- */
- /* package */ static class LogFileManager {
- public static final String RESEARCH_LOG_FILENAME_KEY = "RESEARCH_LOG_FILENAME";
-
- private static final String DEFAULT_FILENAME = "researchLog.txt";
- private static final long LOGFILE_PURGE_INTERVAL = 1000 * 60 * 60 * 24;
-
- protected InputMethodService mIms;
- protected File mFile;
- protected PrintWriter mPrintWriter;
-
- /* package */ LogFileManager() {
- }
-
- public void init(final InputMethodService ims) {
- mIms = ims;
- }
-
- public synchronized void createLogFile() throws IOException {
- createLogFile(DEFAULT_FILENAME);
- }
-
- public synchronized void createLogFile(final SharedPreferences prefs)
- throws IOException {
- final String filename =
- prefs.getString(RESEARCH_LOG_FILENAME_KEY, DEFAULT_FILENAME);
- createLogFile(filename);
- }
-
- public synchronized void createLogFile(final String filename)
- throws IOException {
- if (mIms == null) {
- final String msg = "InputMethodService is not configured. Logging is off.";
- Log.w(TAG, msg);
- throw new IOException(msg);
- }
- final File filesDir = mIms.getFilesDir();
- if (filesDir == null || !filesDir.exists()) {
- final String msg = "Storage directory does not exist. Logging is off.";
- Log.w(TAG, msg);
- throw new IOException(msg);
- }
- close();
- final File file = new File(filesDir, filename);
- mFile = file;
- boolean append = true;
- if (file.exists() && file.lastModified() + LOGFILE_PURGE_INTERVAL <
- System.currentTimeMillis()) {
- append = false;
- }
- mPrintWriter = new PrintWriter(new BufferedWriter(new FileWriter(file, append)), true);
- }
-
- public synchronized boolean append(final String s) {
- PrintWriter printWriter = mPrintWriter;
- if (printWriter == null || !mFile.exists()) {
- if (DEBUG) {
- Log.w(TAG, "PrintWriter is null... attempting to create default log file");
- }
- try {
- createLogFile();
- printWriter = mPrintWriter;
- } catch (IOException e) {
- Log.w(TAG, "Failed to create log file. Not logging.");
- return false;
- }
- }
- printWriter.print(s);
- printWriter.flush();
- return !printWriter.checkError();
- }
-
- public synchronized void reset() {
- if (mPrintWriter != null) {
- mPrintWriter.close();
- mPrintWriter = null;
- if (DEBUG) {
- Log.d(TAG, "logfile closed");
- }
- }
- if (mFile != null) {
- mFile.delete();
- if (DEBUG) {
- Log.d(TAG, "logfile deleted");
- }
- mFile = null;
- }
- }
-
- public synchronized void close() {
- if (mPrintWriter != null) {
- mPrintWriter.close();
- mPrintWriter = null;
- mFile = null;
- if (DEBUG) {
- Log.d(TAG, "logfile closed");
- }
- }
- }
-
- /* package */ synchronized void flush() {
- if (mPrintWriter != null) {
- mPrintWriter.flush();
- }
- }
-
- /* package */ synchronized String getContents() {
- final File file = mFile;
- if (file == null) {
- return "";
- }
- if (mPrintWriter != null) {
- mPrintWriter.flush();
- }
- FileInputStream stream = null;
- FileChannel fileChannel = null;
- String s = "";
- try {
- stream = new FileInputStream(file);
- fileChannel = stream.getChannel();
- final ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
- fileChannel.read(byteBuffer);
- byteBuffer.rewind();
- CharBuffer charBuffer = Charset.defaultCharset().decode(byteBuffer);
- s = charBuffer.toString();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (fileChannel != null) {
- fileChannel.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (stream != null) {
- stream.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- return s;
- }
- }
-
- private ResearchLogger(final LogFileManager logFileManager) {
- final HandlerThread handlerThread = new HandlerThread("ResearchLogger logging task",
- Process.THREAD_PRIORITY_BACKGROUND);
- handlerThread.start();
- mLoggingHandler = new Handler(handlerThread.getLooper());
- mLogFileManager = logFileManager;
- }
-
- public static ResearchLogger getInstance() {
- return sInstance;
- }
-
- public static void init(final InputMethodService ims, final SharedPreferences prefs) {
- sInstance.initInternal(ims, prefs);
- }
-
- /* package */ void initInternal(final InputMethodService ims, final SharedPreferences prefs) {
- mIms = ims;
- final LogFileManager logFileManager = mLogFileManager;
- if (logFileManager != null) {
- logFileManager.init(ims);
- try {
- logFileManager.createLogFile(prefs);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (prefs != null) {
- sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
- prefs.registerOnSharedPreferenceChangeListener(this);
- }
- }
-
- /**
- * Represents a category of logging events that share the same subfield structure.
- */
- private static enum LogGroup {
- MOTION_EVENT("m"),
- KEY("k"),
- CORRECTION("c"),
- STATE_CHANGE("s"),
- UNSTRUCTURED("u");
-
- private final String mLogString;
-
- private LogGroup(final String logString) {
- mLogString = logString;
- }
- }
-
- public void logMotionEvent(final int action, final long eventTime, final int id,
- final int x, final int y, final float size, final float pressure) {
- final String eventTag;
- switch (action) {
- case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break;
- case MotionEvent.ACTION_UP: eventTag = "[Up]"; break;
- case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break;
- case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break;
- case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break;
- case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break;
- case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break;
- default: eventTag = "[Action" + action + "]"; break;
- }
- if (!TextUtils.isEmpty(eventTag)) {
- final StringBuilder sb = new StringBuilder();
- sb.append(eventTag);
- sb.append('\t'); sb.append(eventTime);
- sb.append('\t'); sb.append(id);
- sb.append('\t'); sb.append(x);
- sb.append('\t'); sb.append(y);
- sb.append('\t'); sb.append(size);
- sb.append('\t'); sb.append(pressure);
- write(LogGroup.MOTION_EVENT, sb.toString());
- }
- }
-
- public void logKeyEvent(final int code, final int x, final int y) {
- final StringBuilder sb = new StringBuilder();
- sb.append(Keyboard.printableCode(code));
- sb.append('\t'); sb.append(x);
- sb.append('\t'); sb.append(y);
- write(LogGroup.KEY, sb.toString());
- }
-
- public void logCorrection(final String subgroup, final String before, final String after,
- final int position) {
- final StringBuilder sb = new StringBuilder();
- sb.append(subgroup);
- sb.append('\t'); sb.append(before);
- sb.append('\t'); sb.append(after);
- sb.append('\t'); sb.append(position);
- write(LogGroup.CORRECTION, sb.toString());
- }
-
- public void logStateChange(final String subgroup, final String details) {
- write(LogGroup.STATE_CHANGE, subgroup + "\t" + details);
- }
-
- public static class UnsLogGroup {
- private static final boolean DEFAULT_ENABLED = true;
-
- private static final boolean KEYBOARDSTATE_ONCANCELINPUT_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONCODEINPUT_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONPRESSKEY_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONRELEASEKEY_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_COMMITTEXT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_DELETESURROUNDINGTEXT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_ONDISPLAYCOMPLETIONS_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_ONUPDATESELECTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_PERFORMEDITORACTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_PICKSUGGESTIONMANUALLY_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_REVERTCOMMIT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINIME_REVERTSWAPPUNCTUATION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_SENDKEYCODEPOINT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean
- POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_ONDOWNEVENT_ENABLED = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_ONMOVEEVENT_ENABLED = DEFAULT_ENABLED;
- private static final boolean SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED = DEFAULT_ENABLED;
- }
-
- public static void logUnstructured(String logGroup, final String details) {
- // TODO: improve performance by making entire class static and/or implementing natively
- getInstance().write(LogGroup.UNSTRUCTURED, logGroup + "\t" + details);
- }
-
- private void write(final LogGroup logGroup, final String log) {
- // TODO: rewrite in native for better performance
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- final long currentTime = System.currentTimeMillis();
- final long upTime = SystemClock.uptimeMillis();
- final StringBuilder builder = new StringBuilder();
- builder.append(currentTime);
- builder.append('\t'); builder.append(upTime);
- builder.append('\t'); builder.append(logGroup.mLogString);
- builder.append('\t'); builder.append(log);
- builder.append('\n');
- if (DEBUG) {
- Log.d(TAG, "Write: " + '[' + logGroup.mLogString + ']' + log);
- }
- final String s = builder.toString();
- if (mLogFileManager.append(s)) {
- // success
- } else {
- if (DEBUG) {
- Log.w(TAG, "Unable to write to log.");
- }
- // perhaps logfile was deleted. try to recreate and relog.
- try {
- mLogFileManager.createLogFile(PreferenceManager
- .getDefaultSharedPreferences(mIms));
- mLogFileManager.append(s);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- });
- }
-
- public void clearAll() {
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- if (DEBUG) {
- Log.d(TAG, "Delete log file.");
- }
- mLogFileManager.reset();
- }
- });
- }
-
- /* package */ LogFileManager getLogFileManager() {
- return mLogFileManager;
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- if (key == null || prefs == null) {
- return;
- }
- sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
- }
-
- public static void keyboardState_onCancelInput(final boolean isSinglePointer,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONCANCELINPUT_ENABLED) {
- final String s = "onCancelInput: single=" + isSinglePointer + " " + keyboardState;
- logUnstructured("KeyboardState_onCancelInput", s);
- }
- }
-
- public static void keyboardState_onCodeInput(
- final int code, final boolean isSinglePointer, final int autoCaps,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONCODEINPUT_ENABLED) {
- final String s = "onCodeInput: code=" + Keyboard.printableCode(code)
- + " single=" + isSinglePointer
- + " autoCaps=" + autoCaps + " " + keyboardState;
- logUnstructured("KeyboardState_onCodeInput", s);
- }
- }
-
- public static void keyboardState_onLongPressTimeout(final int code,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED) {
- final String s = "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " "
- + keyboardState;
- logUnstructured("KeyboardState_onLongPressTimeout", s);
- }
- }
-
- public static void keyboardState_onPressKey(final int code,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONPRESSKEY_ENABLED) {
- final String s = "onPressKey: code=" + Keyboard.printableCode(code) + " "
- + keyboardState;
- logUnstructured("KeyboardState_onPressKey", s);
- }
- }
-
- public static void keyboardState_onReleaseKey(final KeyboardState keyboardState, final int code,
- final boolean withSliding) {
- if (UnsLogGroup.KEYBOARDSTATE_ONRELEASEKEY_ENABLED) {
- final String s = "onReleaseKey: code=" + Keyboard.printableCode(code)
- + " sliding=" + withSliding + " " + keyboardState;
- logUnstructured("KeyboardState_onReleaseKey", s);
- }
- }
-
- public static void latinIME_commitCurrentAutoCorrection(final String typedWord,
- final String autoCorrection) {
- if (UnsLogGroup.LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED) {
- if (typedWord.equals(autoCorrection)) {
- getInstance().logCorrection("[----]", typedWord, autoCorrection, -1);
- } else {
- getInstance().logCorrection("[Auto]", typedWord, autoCorrection, -1);
- }
- }
- }
-
- public static void latinIME_commitText(final CharSequence typedWord) {
- if (UnsLogGroup.LATINIME_COMMITTEXT_ENABLED) {
- logUnstructured("LatinIME_commitText", typedWord.toString());
- }
- }
-
- public static void latinIME_deleteSurroundingText(final int length) {
- if (UnsLogGroup.LATINIME_DELETESURROUNDINGTEXT_ENABLED) {
- logUnstructured("LatinIME_deleteSurroundingText", String.valueOf(length));
- }
- }
-
- public static void latinIME_doubleSpaceAutoPeriod() {
- if (UnsLogGroup.LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED) {
- logUnstructured("LatinIME_doubleSpaceAutoPeriod", "");
- }
- }
-
- public static void latinIME_onDisplayCompletions(
- final CompletionInfo[] applicationSpecifiedCompletions) {
- if (UnsLogGroup.LATINIME_ONDISPLAYCOMPLETIONS_ENABLED) {
- final StringBuilder builder = new StringBuilder();
- builder.append("Received completions:");
- if (applicationSpecifiedCompletions != null) {
- for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
- builder.append(" #");
- builder.append(i);
- builder.append(": ");
- builder.append(applicationSpecifiedCompletions[i]);
- builder.append("\n");
- }
- }
- logUnstructured("LatinIME_onDisplayCompletions", builder.toString());
- }
- }
-
- public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
- final SharedPreferences prefs) {
- if (UnsLogGroup.LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED) {
- final StringBuilder builder = new StringBuilder();
- builder.append("onStartInputView: editorInfo:");
- builder.append("\tinputType=");
- builder.append(Integer.toHexString(editorInfo.inputType));
- builder.append("\timeOptions=");
- builder.append(Integer.toHexString(editorInfo.imeOptions));
- builder.append("\tdisplay="); builder.append(Build.DISPLAY);
- builder.append("\tmodel="); builder.append(Build.MODEL);
- for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
- builder.append("\t" + entry.getKey());
- Object value = entry.getValue();
- builder.append("=" + ((value == null) ? "<null>" : value.toString()));
- }
- logUnstructured("LatinIME_onStartInputViewInternal", builder.toString());
- }
- }
-
- public static void latinIME_onUpdateSelection(final int lastSelectionStart,
- final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
- final int newSelStart, final int newSelEnd, final int composingSpanStart,
- final int composingSpanEnd) {
- if (UnsLogGroup.LATINIME_ONUPDATESELECTION_ENABLED) {
- final String s = "onUpdateSelection: oss=" + oldSelStart
- + ", ose=" + oldSelEnd
- + ", lss=" + lastSelectionStart
- + ", lse=" + lastSelectionEnd
- + ", nss=" + newSelStart
- + ", nse=" + newSelEnd
- + ", cs=" + composingSpanStart
- + ", ce=" + composingSpanEnd;
- logUnstructured("LatinIME_onUpdateSelection", s);
- }
- }
-
- public static void latinIME_performEditorAction(final int imeActionNext) {
- if (UnsLogGroup.LATINIME_PERFORMEDITORACTION_ENABLED) {
- logUnstructured("LatinIME_performEditorAction", String.valueOf(imeActionNext));
- }
- }
-
- public static void latinIME_pickApplicationSpecifiedCompletion(final int index,
- final CharSequence text, int x, int y) {
- if (UnsLogGroup.LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED) {
- final String s = String.valueOf(index) + '\t' + text + '\t' + x + '\t' + y;
- logUnstructured("LatinIME_pickApplicationSpecifiedCompletion", s);
- }
- }
-
- public static void latinIME_pickSuggestionManually(final String replacedWord,
- final int index, CharSequence suggestion, int x, int y) {
- if (UnsLogGroup.LATINIME_PICKSUGGESTIONMANUALLY_ENABLED) {
- final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
- logUnstructured("LatinIME_pickSuggestionManually", s);
- }
- }
-
- public static void latinIME_punctuationSuggestion(final int index,
- final CharSequence suggestion, int x, int y) {
- if (UnsLogGroup.LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED) {
- final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
- logUnstructured("LatinIME_pickPunctuationSuggestion", s);
- }
- }
-
- public static void latinIME_revertDoubleSpaceWhileInBatchEdit() {
- if (UnsLogGroup.LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED) {
- logUnstructured("LatinIME_revertDoubleSpaceWhileInBatchEdit", "");
- }
- }
-
- public static void latinIME_revertSwapPunctuation() {
- if (UnsLogGroup.LATINIME_REVERTSWAPPUNCTUATION_ENABLED) {
- logUnstructured("LatinIME_revertSwapPunctuation", "");
- }
- }
-
- public static void latinIME_sendKeyCodePoint(final int code) {
- if (UnsLogGroup.LATINIME_SENDKEYCODEPOINT_ENABLED) {
- logUnstructured("LatinIME_sendKeyCodePoint", String.valueOf(code));
- }
- }
-
- public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() {
- if (UnsLogGroup.LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED) {
- logUnstructured("latinIME_swapSwapperAndSpaceWhileInBatchEdit", "");
- }
- }
-
- public static void latinIME_switchToKeyboardView() {
- if (UnsLogGroup.LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED) {
- final String s = "Switch to keyboard view.";
- logUnstructured("LatinIME_switchToKeyboardView", s);
- }
- }
-
- public static void latinKeyboardView_onLongPress() {
- if (UnsLogGroup.LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED) {
- final String s = "long press detected";
- logUnstructured("LatinKeyboardView_onLongPress", s);
- }
- }
-
- public static void latinKeyboardView_processMotionEvent(MotionEvent me, int action,
- long eventTime, int index, int id, int x, int y) {
- if (UnsLogGroup.LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED) {
- final float size = me.getSize(index);
- final float pressure = me.getPressure(index);
- if (action != MotionEvent.ACTION_MOVE) {
- getInstance().logMotionEvent(action, eventTime, id, x, y, size, pressure);
- }
- }
- }
-
- public static void latinKeyboardView_setKeyboard(final Keyboard keyboard) {
- if (UnsLogGroup.LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED) {
- StringBuilder builder = new StringBuilder();
- builder.append("id=");
- builder.append(keyboard.mId);
- builder.append("\tw=");
- builder.append(keyboard.mOccupiedWidth);
- builder.append("\th=");
- builder.append(keyboard.mOccupiedHeight);
- builder.append("\tkeys=[");
- boolean first = true;
- for (Key key : keyboard.mKeys) {
- if (first) {
- first = false;
- } else {
- builder.append(",");
- }
- builder.append("{code:");
- builder.append(key.mCode);
- builder.append(",altCode:");
- builder.append(key.mAltCode);
- builder.append(",x:");
- builder.append(key.mX);
- builder.append(",y:");
- builder.append(key.mY);
- builder.append(",w:");
- builder.append(key.mWidth);
- builder.append(",h:");
- builder.append(key.mHeight);
- builder.append("}");
- }
- builder.append("]");
- logUnstructured("LatinKeyboardView_setKeyboard", builder.toString());
- }
- }
-
- public static void latinIME_revertCommit(final String originallyTypedWord) {
- if (UnsLogGroup.LATINIME_REVERTCOMMIT_ENABLED) {
- logUnstructured("LatinIME_revertCommit", originallyTypedWord);
- }
- }
-
- public static void pointerTracker_callListenerOnCancelInput() {
- final String s = "onCancelInput";
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED) {
- logUnstructured("PointerTracker_callListenerOnCancelInput", s);
- }
- }
-
- public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
- final int y, final boolean ignoreModifierKey, final boolean altersCode,
- final int code) {
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED) {
- final String s = "onCodeInput: " + Keyboard.printableCode(code)
- + " text=" + key.mOutputText + " x=" + x + " y=" + y
- + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
- + " enabled=" + key.isEnabled();
- logUnstructured("PointerTracker_callListenerOnCodeInput", s);
- }
- }
-
- public static void pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(
- final Key key, final boolean ignoreModifierKey) {
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED) {
- final String s = "onPress : " + KeyDetector.printableCode(key)
- + " ignoreModifier=" + ignoreModifierKey
- + " enabled=" + key.isEnabled();
- logUnstructured("PointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange", s);
- }
- }
-
- public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
- final boolean withSliding, final boolean ignoreModifierKey) {
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED) {
- final String s = "onRelease : " + Keyboard.printableCode(primaryCode)
- + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey
- + " enabled="+ key.isEnabled();
- logUnstructured("PointerTracker_callListenerOnRelease", s);
- }
- }
-
- public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
- if (UnsLogGroup.POINTERTRACKER_ONDOWNEVENT_ENABLED) {
- final String s = "onDownEvent: ignore potential noise: time=" + deltaT
- + " distance=" + distanceSquared;
- logUnstructured("PointerTracker_onDownEvent", s);
- }
- }
-
- public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
- final int lastY) {
- if (UnsLogGroup.POINTERTRACKER_ONMOVEEVENT_ENABLED) {
- final String s = String.format("onMoveEvent: sudden move is translated to "
- + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y);
- logUnstructured("PointerTracker_onMoveEvent", s);
- }
- }
-
- public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
- if (UnsLogGroup.SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED) {
- final String s = "onTouchEvent: ignore sudden jump " + me;
- logUnstructured("SuddenJumpingTouchEventHandler_onTouchEvent", s);
- }
- }
-
- public static void suggestionsView_setSuggestions(final SuggestedWords mSuggestedWords) {
- if (UnsLogGroup.SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED) {
- logUnstructured("SuggestionsView_setSuggestions", mSuggestedWords.toString());
- }
- }
-} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/ResizableIntArray.java
new file mode 100644
index 000000000..c660f92c4
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ResizableIntArray.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin;
+
+import java.util.Arrays;
+
+// TODO: This class is not thread-safe.
+public class ResizableIntArray {
+ private int[] mArray;
+ private int mLength;
+
+ public ResizableIntArray(final int capacity) {
+ reset(capacity);
+ }
+
+ public int get(final int index) {
+ if (index < mLength) {
+ return mArray[index];
+ }
+ throw new ArrayIndexOutOfBoundsException("length=" + mLength + "; index=" + index);
+ }
+
+ public void add(final int index, final int val) {
+ if (index < mLength) {
+ mArray[index] = val;
+ } else {
+ mLength = index;
+ add(val);
+ }
+ }
+
+ public void add(final int val) {
+ final int currentLength = mLength;
+ ensureCapacity(currentLength + 1);
+ mArray[currentLength] = val;
+ mLength = currentLength + 1;
+ }
+
+ /**
+ * Calculate the new capacity of {@code mArray}.
+ * @param minimumCapacity the minimum capacity that the {@code mArray} should have.
+ * @return the new capacity that the {@code mArray} should have. Returns zero when there is no
+ * need to expand {@code mArray}.
+ */
+ private int calculateCapacity(final int minimumCapacity) {
+ final int currentCapcity = mArray.length;
+ if (currentCapcity < minimumCapacity) {
+ final int nextCapacity = currentCapcity * 2;
+ // The following is the same as return Math.max(minimumCapacity, nextCapacity);
+ return minimumCapacity > nextCapacity ? minimumCapacity : nextCapacity;
+ }
+ return 0;
+ }
+
+ private void ensureCapacity(final int minimumCapacity) {
+ final int newCapacity = calculateCapacity(minimumCapacity);
+ if (newCapacity > 0) {
+ // TODO: Implement primitive array pool.
+ mArray = Arrays.copyOf(mArray, newCapacity);
+ }
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ public void setLength(final int newLength) {
+ ensureCapacity(newLength);
+ mLength = newLength;
+ }
+
+ public void reset(final int capacity) {
+ // TODO: Implement primitive array pool.
+ mArray = new int[capacity];
+ mLength = 0;
+ }
+
+ public int[] getPrimitiveArray() {
+ return mArray;
+ }
+
+ public void set(final ResizableIntArray ip) {
+ // TODO: Implement primitive array pool.
+ mArray = ip.mArray;
+ mLength = ip.mLength;
+ }
+
+ public void copy(final ResizableIntArray ip) {
+ final int newCapacity = calculateCapacity(ip.mLength);
+ if (newCapacity > 0) {
+ // TODO: Implement primitive array pool.
+ mArray = new int[newCapacity];
+ }
+ System.arraycopy(ip.mArray, 0, mArray, 0, ip.mLength);
+ mLength = ip.mLength;
+ }
+
+ public void append(final ResizableIntArray src, final int startPos, final int length) {
+ if (length == 0) {
+ return;
+ }
+ final int currentLength = mLength;
+ final int newLength = currentLength + length;
+ ensureCapacity(newLength);
+ System.arraycopy(src.mArray, startPos, mArray, currentLength, length);
+ mLength = newLength;
+ }
+
+ public void fill(final int value, final int startPos, final int length) {
+ if (startPos < 0 || length < 0) {
+ throw new IllegalArgumentException("startPos=" + startPos + "; length=" + length);
+ }
+ final int endPos = startPos + length;
+ ensureCapacity(endPos);
+ Arrays.fill(mArray, startPos, endPos, value);
+ if (mLength < endPos) {
+ mLength = endPos;
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mLength; i++) {
+ if (i != 0) {
+ sb.append(",");
+ }
+ sb.append(mArray[i]);
+ }
+ return "[" + sb + "]";
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
new file mode 100644
index 000000000..41e59e92d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin;
+
+import android.inputmethodservice.InputMethodService;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
+
+import java.util.regex.Pattern;
+
+/**
+ * Wrapper for InputConnection to simplify interaction
+ */
+public class RichInputConnection {
+ private static final String TAG = RichInputConnection.class.getSimpleName();
+ private static final boolean DBG = false;
+ // Provision for a long word pair and a separator
+ private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
+ private static final Pattern spaceRegex = Pattern.compile("\\s+");
+ private static final int INVALID_CURSOR_POSITION = -1;
+
+ private final InputMethodService mParent;
+ InputConnection mIC;
+ int mNestLevel;
+ public RichInputConnection(final InputMethodService parent) {
+ mParent = parent;
+ mIC = null;
+ mNestLevel = 0;
+ }
+
+ public void beginBatchEdit() {
+ if (++mNestLevel == 1) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) {
+ mIC.beginBatchEdit();
+ }
+ } else {
+ if (DBG) {
+ throw new RuntimeException("Nest level too deep");
+ } else {
+ Log.e(TAG, "Nest level too deep : " + mNestLevel);
+ }
+ }
+ }
+ public void endBatchEdit() {
+ if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
+ if (--mNestLevel == 0 && null != mIC) {
+ mIC.endBatchEdit();
+ }
+ }
+
+ private void checkBatchEdit() {
+ if (mNestLevel != 1) {
+ // TODO: exception instead
+ Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
+ Log.e(TAG, Utils.getStackTrace(4));
+ }
+ }
+
+ public void finishComposingText() {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.finishComposingText();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_finishComposingText();
+ }
+ }
+ }
+
+ public void commitText(final CharSequence text, final int i) {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.commitText(text, i);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitText(text, i);
+ }
+ }
+ }
+
+ public int getCursorCapsMode(final int inputType) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
+ return mIC.getCursorCapsMode(inputType);
+ }
+
+ public CharSequence getTextBeforeCursor(final int i, final int j) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) return mIC.getTextBeforeCursor(i, j);
+ return null;
+ }
+
+ public CharSequence getTextAfterCursor(final int i, final int j) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) return mIC.getTextAfterCursor(i, j);
+ return null;
+ }
+
+ public void deleteSurroundingText(final int i, final int j) {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.deleteSurroundingText(i, j);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
+ }
+ }
+ }
+
+ public void performEditorAction(final int actionId) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) {
+ mIC.performEditorAction(actionId);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_performEditorAction(actionId);
+ }
+ }
+ }
+
+ public void sendKeyEvent(final KeyEvent keyEvent) {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.sendKeyEvent(keyEvent);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_sendKeyEvent(keyEvent);
+ }
+ }
+ }
+
+ public void setComposingText(final CharSequence text, final int i) {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.setComposingText(text, i);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_setComposingText(text, i);
+ }
+ }
+ }
+
+ public void setSelection(final int from, final int to) {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.setSelection(from, to);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_setSelection(from, to);
+ }
+ }
+ }
+
+ public void commitCorrection(final CorrectionInfo correctionInfo) {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.commitCorrection(correctionInfo);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitCorrection(correctionInfo);
+ }
+ }
+ }
+
+ public void commitCompletion(final CompletionInfo completionInfo) {
+ checkBatchEdit();
+ if (null != mIC) {
+ mIC.commitCompletion(completionInfo);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitCompletion(completionInfo);
+ }
+ }
+ }
+
+ public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null == mIC) return null;
+ final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+ return getNthPreviousWord(prev, sentenceSeperators, n);
+ }
+
+ /**
+ * Represents a range of text, relative to the current cursor position.
+ */
+ public static class Range {
+ /** Characters before selection start */
+ public final int mCharsBefore;
+
+ /**
+ * Characters after selection start, including one trailing word
+ * separator.
+ */
+ public final int mCharsAfter;
+
+ /** The actual characters that make up a word */
+ public final String mWord;
+
+ public Range(int charsBefore, int charsAfter, String word) {
+ if (charsBefore < 0 || charsAfter < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ this.mCharsBefore = charsBefore;
+ this.mCharsAfter = charsAfter;
+ this.mWord = word;
+ }
+ }
+
+ private static boolean isSeparator(int code, String sep) {
+ return sep.indexOf(code) != -1;
+ }
+
+ // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
+ // n = 2 retrieves the word before that, and so on. This splits on whitespace only.
+ // Also, it won't return words that end in a separator (if the nth word before the cursor
+ // ends in a separator, it returns null).
+ // Example :
+ // (n = 1) "abc def|" -> def
+ // (n = 1) "abc def |" -> def
+ // (n = 1) "abc def. |" -> null
+ // (n = 1) "abc def . |" -> null
+ // (n = 2) "abc def|" -> abc
+ // (n = 2) "abc def |" -> abc
+ // (n = 2) "abc def. |" -> abc
+ // (n = 2) "abc def . |" -> def
+ // (n = 2) "abc|" -> null
+ // (n = 2) "abc |" -> null
+ // (n = 2) "abc. def|" -> null
+ public static CharSequence getNthPreviousWord(final CharSequence prev,
+ final String sentenceSeperators, final int n) {
+ if (prev == null) return null;
+ String[] w = spaceRegex.split(prev);
+
+ // If we can't find n words, or we found an empty word, return null.
+ if (w.length < n || w[w.length - n].length() <= 0) return null;
+
+ // If ends in a separator, return null
+ char lastChar = w[w.length - n].charAt(w[w.length - n].length() - 1);
+ if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+ return w[w.length - n];
+ }
+
+ /**
+ * @param separators characters which may separate words
+ * @return the word that surrounds the cursor, including up to one trailing
+ * separator. For example, if the field contains "he|llo world", where |
+ * represents the cursor, then "hello " will be returned.
+ */
+ public String getWordAtCursor(String separators) {
+ // getWordRangeAtCursor returns null if the connection is null
+ Range r = getWordRangeAtCursor(separators, 0);
+ return (r == null) ? null : r.mWord;
+ }
+
+ private int getCursorPosition() {
+ mIC = mParent.getCurrentInputConnection();
+ if (null == mIC) return INVALID_CURSOR_POSITION;
+ final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0);
+ if (extracted == null) {
+ return INVALID_CURSOR_POSITION;
+ }
+ return extracted.startOffset + extracted.selectionStart;
+ }
+
+ /**
+ * Returns the text surrounding the cursor.
+ *
+ * @param sep a string of characters that split words.
+ * @param additionalPrecedingWordsCount the number of words before the current word that should
+ * be included in the returned range
+ * @return a range containing the text surrounding the cursor
+ */
+ public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) {
+ mIC = mParent.getCurrentInputConnection();
+ if (mIC == null || sep == null) {
+ return null;
+ }
+ CharSequence before = mIC.getTextBeforeCursor(1000, 0);
+ CharSequence after = mIC.getTextAfterCursor(1000, 0);
+ if (before == null || after == null) {
+ return null;
+ }
+
+ // Going backward, alternate skipping non-separators and separators until enough words
+ // have been read.
+ int start = before.length();
+ boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at
+ while (true) { // see comments below for why this is guaranteed to halt
+ while (start > 0) {
+ final int codePoint = Character.codePointBefore(before, start);
+ if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+ break; // inner loop
+ }
+ --start;
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ --start;
+ }
+ }
+ // isStoppingAtWhitespace is true every other time through the loop,
+ // so additionalPrecedingWordsCount is guaranteed to become < 0, which
+ // guarantees outer loop termination
+ if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) {
+ break; // outer loop
+ }
+ isStoppingAtWhitespace = !isStoppingAtWhitespace;
+ }
+
+ // Find last word separator after the cursor
+ int end = -1;
+ while (++end < after.length()) {
+ final int codePoint = Character.codePointAt(after, end);
+ if (isSeparator(codePoint, sep)) {
+ break;
+ }
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ ++end;
+ }
+ }
+
+ int cursor = getCursorPosition();
+ if (start >= 0 && cursor + end <= after.length() + before.length()) {
+ String word = before.toString().substring(start, before.length())
+ + after.toString().substring(0, end);
+ return new Range(before.length() - start, end, word);
+ }
+
+ return null;
+ }
+
+ public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
+ CharSequence before = getTextBeforeCursor(1, 0);
+ CharSequence after = getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
+ && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
+ && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
+ return true;
+ }
+ return false;
+ }
+
+ public void removeTrailingSpace() {
+ checkBatchEdit();
+ final CharSequence lastOne = getTextBeforeCursor(1, 0);
+ if (lastOne != null && lastOne.length() == 1
+ && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
+ deleteSurroundingText(1, 0);
+ }
+ }
+
+ public boolean sameAsTextBeforeCursor(final CharSequence text) {
+ final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
+ return TextUtils.equals(text, beforeText);
+ }
+
+ /* (non-javadoc)
+ * Returns the word before the cursor if the cursor is at the end of a word, null otherwise
+ */
+ public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
+ // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
+ // separator or end of line/text)
+ // Example: "test|"<EOL> "te|st" get rejected here
+ final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(textAfterCursor)
+ && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
+
+ // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
+ // Example: " -|" gets rejected here but "e-|" and "e|" are okay
+ CharSequence word = getWordAtCursor(settings.mWordSeparators);
+ // We don't suggest on leading single quotes, so we have to remove them from the word if
+ // it starts with single quotes.
+ while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
+ word = word.subSequence(1, word.length());
+ }
+ if (TextUtils.isEmpty(word)) return null;
+ // Find the last code point of the string
+ final int lastCodePoint = Character.codePointBefore(word, word.length());
+ // If for some reason the text field contains non-unicode binary data, or if the
+ // charsequence is exactly one char long and the contents is a low surrogate, return null.
+ if (!Character.isDefined(lastCodePoint)) return null;
+ // Bail out if the cursor is not at the end of a word (cursor must be preceded by
+ // non-whitespace, non-separator, non-start-of-text)
+ // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
+ if (settings.isWordSeparator(lastCodePoint)) return null;
+ final char firstChar = word.charAt(0); // we just tested that word is not empty
+ if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
+
+ // We only suggest on words that start with a letter or a symbol that is excluded from
+ // word separators (see #handleCharacterWhileInBatchEdit).
+ if (!(Character.isLetter(firstChar)
+ || settings.isSymbolExcludedFromWordSeparators(firstChar))) {
+ return null;
+ }
+
+ return word;
+ }
+
+ public boolean revertDoubleSpace() {
+ checkBatchEdit();
+ // Here we test whether we indeed have a period and a space before us. This should not
+ // be needed, but it's there just in case something went wrong.
+ final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+ if (!". ".equals(textBeforeCursor)) {
+ // Theoretically we should not be coming here if there isn't ". " before the
+ // cursor, but the application may be changing the text while we are typing, so
+ // anything goes. We should not crash.
+ Log.d(TAG, "Tried to revert double-space combo but we didn't find "
+ + "\". \" just before the cursor.");
+ return false;
+ }
+ deleteSurroundingText(2, 0);
+ commitText(" ", 1);
+ return true;
+ }
+
+ public boolean revertSwapPunctuation() {
+ checkBatchEdit();
+ // Here we test whether we indeed have a space and something else before us. This should not
+ // be needed, but it's there just in case something went wrong.
+ final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+ // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
+ // enter surrogate pairs this code will have been removed.
+ if (TextUtils.isEmpty(textBeforeCursor)
+ || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
+ // We may only come here if the application is changing the text while we are typing.
+ // This is quite a broken case, but not logically impossible, so we shouldn't crash,
+ // but some debugging log may be in order.
+ Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
+ + "find a space just before the cursor.");
+ return false;
+ }
+ deleteSurroundingText(2, 0);
+ commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 4bb21720b..6251c9acd 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -39,6 +39,7 @@ import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
import com.android.inputmethodcommon.InputMethodSettingsFragment;
public class Settings extends InputMethodSettingsFragment
@@ -61,6 +62,7 @@ public class Settings extends InputMethodSettingsFragment
public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME =
"last_user_dictionary_write_time";
public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings";
+ public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
public static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
"pref_suppress_language_switch_key";
public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
@@ -68,14 +70,15 @@ public class Settings extends InputMethodSettingsFragment
public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles";
public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
"pref_key_preview_popup_dismiss_delay";
- public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
- public static final String PREF_BIGRAM_SUGGESTION = "next_word_suggestion";
public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
- public static final String PREF_KEY_ENABLE_SPAN_INSERT = "enable_span_insert";
+ public static final String PREF_GESTURE_INPUT = "gesture_input";
public static final String PREF_VIBRATION_DURATION_SETTINGS =
"pref_vibration_duration_settings";
public static final String PREF_KEYPRESS_SOUND_VOLUME =
"pref_keypress_sound_volume";
+ public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
+ public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT =
+ "pref_gesture_floating_preview_text";
public static final String PREF_INPUT_LANGUAGE = "input_language";
public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
@@ -87,23 +90,24 @@ public class Settings extends InputMethodSettingsFragment
private ListPreference mShowCorrectionSuggestionsPreference;
private ListPreference mAutoCorrectionThresholdPreference;
private ListPreference mKeyPreviewPopupDismissDelay;
- // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
- private CheckBoxPreference mBigramSuggestion;
- // Prediction: use bigrams to predict the next word when there is no input for it yet
+ // Use bigrams to predict the next word when there is no input for it yet
private CheckBoxPreference mBigramPrediction;
private Preference mDebugSettingsPreference;
private TextView mKeypressVibrationDurationSettingsTextView;
private TextView mKeypressSoundVolumeSettingsTextView;
+ private static void setPreferenceEnabled(Preference preference, boolean enabled) {
+ if (preference != null) {
+ preference.setEnabled(enabled);
+ }
+ }
+
private void ensureConsistencyOfAutoCorrectionSettings() {
final String autoCorrectionOff = getResources().getString(
R.string.auto_correction_threshold_mode_index_off);
final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
- mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff));
- if (null != mBigramPrediction) {
- mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
- }
+ setPreferenceEnabled(mBigramPrediction, !currentSetting.equals(autoCorrectionOff));
}
@Override
@@ -128,7 +132,6 @@ public class Settings extends InputMethodSettingsFragment
mAutoCorrectionThresholdPreference =
(ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
- mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTION);
mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS);
mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
if (mDebugSettingsPreference != null) {
@@ -155,9 +158,6 @@ public class Settings extends InputMethodSettingsFragment
final PreferenceGroup advancedSettings =
(PreferenceGroup) findPreference(PREF_ADVANCED_SETTINGS);
- // Remove those meaningless options for now. TODO: delete them for good
- advancedSettings.removePreference(findPreference(PREF_BIGRAM_SUGGESTION));
- advancedSettings.removePreference(findPreference(PREF_KEY_ENABLE_SPAN_INSERT));
if (!VibratorUtils.getInstance(context).hasVibrator()) {
generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
@@ -165,43 +165,35 @@ public class Settings extends InputMethodSettingsFragment
}
}
- final boolean showPopupOption = res.getBoolean(
+ final boolean showKeyPreviewPopupOption = res.getBoolean(
R.bool.config_enable_show_popup_on_keypress_option);
- if (!showPopupOption) {
+ mKeyPreviewPopupDismissDelay =
+ (ListPreference) findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ if (!showKeyPreviewPopupOption) {
generalSettings.removePreference(findPreference(PREF_POPUP_ON));
- }
-
- final boolean showBigramSuggestionsOption = res.getBoolean(
- R.bool.config_enable_next_word_suggestions_option);
- if (!showBigramSuggestionsOption) {
- textCorrectionGroup.removePreference(mBigramSuggestion);
- if (null != mBigramPrediction) {
- textCorrectionGroup.removePreference(mBigramPrediction);
+ if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
+ advancedSettings.removePreference(mKeyPreviewPopupDismissDelay);
}
+ } else {
+ final String[] entries = new String[] {
+ res.getString(R.string.key_preview_popup_dismiss_no_delay),
+ res.getString(R.string.key_preview_popup_dismiss_default_delay),
+ };
+ final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
+ R.integer.config_key_preview_linger_timeout));
+ mKeyPreviewPopupDismissDelay.setEntries(entries);
+ mKeyPreviewPopupDismissDelay.setEntryValues(
+ new String[] { "0", popupDismissDelayDefaultValue });
+ if (null == mKeyPreviewPopupDismissDelay.getValue()) {
+ mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
+ }
+ setPreferenceEnabled(mKeyPreviewPopupDismissDelay,
+ SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
}
- final CheckBoxPreference includeOtherImesInLanguageSwitchList =
- (CheckBoxPreference)findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
- includeOtherImesInLanguageSwitchList.setEnabled(
+ setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
!SettingsValues.isLanguageSwitchKeySupressed(prefs));
- mKeyPreviewPopupDismissDelay =
- (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
- final String[] entries = new String[] {
- res.getString(R.string.key_preview_popup_dismiss_no_delay),
- res.getString(R.string.key_preview_popup_dismiss_default_delay),
- };
- final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
- R.integer.config_key_preview_linger_timeout));
- mKeyPreviewPopupDismissDelay.setEntries(entries);
- mKeyPreviewPopupDismissDelay.setEntryValues(
- new String[] { "0", popupDismissDelayDefaultValue });
- if (null == mKeyPreviewPopupDismissDelay.getValue()) {
- mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
- }
- mKeyPreviewPopupDismissDelay.setEnabled(
- SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
-
final PreferenceScreen dictionaryLink =
(PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY);
final Intent intent = dictionaryLink.getIntent();
@@ -211,6 +203,21 @@ public class Settings extends InputMethodSettingsFragment
textCorrectionGroup.removePreference(dictionaryLink);
}
+ final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
+ R.bool.config_gesture_input_enabled_by_build_config);
+ final Preference gesturePreviewTrail = findPreference(PREF_GESTURE_PREVIEW_TRAIL);
+ final Preference gestureFloatingPreviewText = findPreference(
+ PREF_GESTURE_FLOATING_PREVIEW_TEXT);
+ if (!gestureInputEnabledByBuildConfig) {
+ miscSettings.removePreference(findPreference(PREF_GESTURE_INPUT));
+ miscSettings.removePreference(gesturePreviewTrail);
+ miscSettings.removePreference(gestureFloatingPreviewText);
+ } else {
+ final boolean gestureInputEnabledByUser = prefs.getBoolean(PREF_GESTURE_INPUT, true);
+ setPreferenceEnabled(gesturePreviewTrail, gestureInputEnabledByUser);
+ setPreferenceEnabled(gestureFloatingPreviewText, gestureInputEnabledByUser);
+ }
+
final boolean showUsabilityStudyModeOption =
res.getBoolean(R.bool.config_enable_usability_study_mode_option)
|| ProductionFlag.IS_EXPERIMENTAL || ENABLE_EXPERIMENTAL_SETTINGS;
@@ -223,7 +230,8 @@ public class Settings extends InputMethodSettingsFragment
if (ProductionFlag.IS_EXPERIMENTAL) {
if (usabilityStudyPref instanceof CheckBoxPreference) {
CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
- checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE, true));
+ checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE,
+ ResearchLogger.DEFAULT_USABILITY_STUDY_MODE));
checkbox.setSummary(R.string.settings_warning_researcher_mode);
}
}
@@ -283,17 +291,22 @@ public class Settings extends InputMethodSettingsFragment
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
(new BackupManager(getActivity())).dataChanged();
if (key.equals(PREF_POPUP_ON)) {
- final ListPreference popupDismissDelay =
- (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
- if (null != popupDismissDelay) {
- popupDismissDelay.setEnabled(prefs.getBoolean(PREF_POPUP_ON, true));
- }
+ setPreferenceEnabled(findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY),
+ prefs.getBoolean(PREF_POPUP_ON, true));
} else if (key.equals(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
- final CheckBoxPreference includeOtherImesInLanguageSwicthList =
- (CheckBoxPreference)findPreference(
- PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
- includeOtherImesInLanguageSwicthList.setEnabled(
+ setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
!SettingsValues.isLanguageSwitchKeySupressed(prefs));
+ } else if (key.equals(PREF_GESTURE_INPUT)) {
+ final boolean gestureInputEnabledByConfig = getResources().getBoolean(
+ R.bool.config_gesture_input_enabled_by_build_config);
+ if (gestureInputEnabledByConfig) {
+ final boolean gestureInputEnabledByUser = prefs.getBoolean(
+ PREF_GESTURE_INPUT, true);
+ setPreferenceEnabled(findPreference(PREF_GESTURE_PREVIEW_TRAIL),
+ gestureInputEnabledByUser);
+ setPreferenceEnabled(findPreference(PREF_GESTURE_FLOATING_PREVIEW_TEXT),
+ gestureInputEnabledByUser);
+ }
}
ensureConsistencyOfAutoCorrectionSettings();
updateVoiceModeSummary();
@@ -327,28 +340,32 @@ public class Settings extends InputMethodSettingsFragment
private void updateKeyPreviewPopupDelaySummary() {
final ListPreference lp = mKeyPreviewPopupDismissDelay;
- lp.setSummary(lp.getEntries()[lp.findIndexOfValue(lp.getValue())]);
+ final CharSequence[] entries = lp.getEntries();
+ if (entries == null || entries.length <= 0) return;
+ lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]);
}
private void updateVoiceModeSummary() {
mVoicePreference.setSummary(
getResources().getStringArray(R.array.voice_input_modes_summary)
- [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
+ [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
}
private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
SharedPreferences sp, Resources res) {
if (mKeypressVibrationDurationSettingsPref != null) {
- final boolean hasVibrator = VibratorUtils.getInstance(getActivity()).hasVibrator();
- final boolean vibrateOn = hasVibrator && sp.getBoolean(Settings.PREF_VIBRATE_ON,
+ final boolean hasVibratorHardware = VibratorUtils.getInstance(getActivity())
+ .hasVibrator();
+ final boolean vibrateOnByUser = sp.getBoolean(Settings.PREF_VIBRATE_ON,
res.getBoolean(R.bool.config_default_vibration_enabled));
- mKeypressVibrationDurationSettingsPref.setEnabled(vibrateOn);
+ setPreferenceEnabled(mKeypressVibrationDurationSettingsPref,
+ hasVibratorHardware && vibrateOnByUser);
}
if (mKeypressSoundVolumeSettingsPref != null) {
final boolean soundOn = sp.getBoolean(Settings.PREF_SOUND_ON,
res.getBoolean(R.bool.config_default_sound_enabled));
- mKeypressSoundVolumeSettingsPref.setEnabled(soundOn);
+ setPreferenceEnabled(mKeypressSoundVolumeSettingsPref, soundOn);
}
}
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index b07c3e59f..dcd2532c1 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
@@ -29,7 +30,6 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
-import java.util.Map;
/**
* When you call the constructor of this class, you may want to change the current system locale by
@@ -38,6 +38,19 @@ import java.util.Map;
public class SettingsValues {
private static final String TAG = SettingsValues.class.getSimpleName();
+ private static final int SUGGESTION_VISIBILITY_SHOW_VALUE
+ = R.string.prefs_suggestion_visibility_show_value;
+ private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+ = R.string.prefs_suggestion_visibility_show_only_portrait_value;
+ private static final int SUGGESTION_VISIBILITY_HIDE_VALUE
+ = R.string.prefs_suggestion_visibility_hide_value;
+
+ private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
+ SUGGESTION_VISIBILITY_SHOW_VALUE,
+ SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE,
+ SUGGESTION_VISIBILITY_HIDE_VALUE
+ };
+
// From resources:
public final int mDelayUpdateOldSuggestions;
public final String mWeakSpaceStrippers;
@@ -63,27 +76,33 @@ public class SettingsValues {
@SuppressWarnings("unused") // TODO: Use this
private final String mKeyPreviewPopupDismissDelayRawValue;
public final boolean mUseContactsDict;
- // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
- public final boolean mBigramSuggestionEnabled;
- // Prediction: use bigrams to predict the next word when there is no input for it yet
+ // Use bigrams to predict the next word when there is no input for it yet
public final boolean mBigramPredictionEnabled;
- public final boolean mEnableSuggestionSpanInsertion;
@SuppressWarnings("unused") // TODO: Use this
private final int mVibrationDurationSettingsRawValue;
@SuppressWarnings("unused") // TODO: Use this
private final float mKeypressSoundVolumeRawValue;
private final InputMethodSubtype[] mAdditionalSubtypes;
+ public final boolean mGestureInputEnabled;
+ public final boolean mGesturePreviewTrailEnabled;
+ public final boolean mGestureFloatingPreviewTextEnabled;
+
+ // From the input box
+ private final InputAttributes mInputAttributes;
// Deduced settings
public final int mKeypressVibrationDuration;
public final float mFxVolume;
public final int mKeyPreviewPopupDismissDelay;
- public final boolean mAutoCorrectEnabled;
+ private final boolean mAutoCorrectEnabled;
public final float mAutoCorrectionThreshold;
+ public final boolean mCorrectionEnabled;
+ public final int mSuggestionVisibility;
private final boolean mVoiceKeyEnabled;
private final boolean mVoiceKeyOnMain;
- public SettingsValues(final SharedPreferences prefs, final Context context) {
+ public SettingsValues(final SharedPreferences prefs, final InputAttributes inputAttributes,
+ final Context context) {
final Resources res = context.getResources();
// Get the resources
@@ -109,6 +128,13 @@ public class SettingsValues {
mSymbolsExcludedFromWordSeparators, res);
mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
+ // Store the input attributes
+ if (null == inputAttributes) {
+ mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+ } else {
+ mInputAttributes = inputAttributes;
+ }
+
// Get the settings preferences
mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
mVibrateOn = isVibrateOn(context, prefs, res);
@@ -131,12 +157,7 @@ public class SettingsValues {
Integer.toString(res.getInteger(R.integer.config_key_preview_linger_timeout)));
mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
mAutoCorrectEnabled = isAutoCorrectEnabled(res, mAutoCorrectionThresholdRawValue);
- mBigramSuggestionEnabled = mAutoCorrectEnabled
- && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled);
- mBigramPredictionEnabled = mBigramSuggestionEnabled
- && isBigramPredictionEnabled(prefs, res);
- // TODO: remove mEnableSuggestionSpanInsertion. It's always true.
- mEnableSuggestionSpanInsertion = true;
+ mBigramPredictionEnabled = isBigramPredictionEnabled(prefs, res);
mVibrationDurationSettingsRawValue =
prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
mKeypressSoundVolumeRawValue = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
@@ -151,22 +172,30 @@ public class SettingsValues {
mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain);
mAdditionalSubtypes = AdditionalSubtype.createAdditionalSubtypesArray(
getPrefAdditionalSubtypes(prefs, res));
+ final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
+ R.bool.config_gesture_input_enabled_by_build_config);
+ mGestureInputEnabled = gestureInputEnabledByBuildConfig
+ && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true);
+ mGesturePreviewTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
+ mGestureFloatingPreviewTextEnabled = prefs.getBoolean(
+ Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true);
+ mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
+ mSuggestionVisibility = createSuggestionVisibility(res);
}
// Helper functions to create member values.
private static SuggestedWords createSuggestPuncList(final String[] puncs) {
- final ArrayList<SuggestedWords.SuggestedWordInfo> puncList =
- new ArrayList<SuggestedWords.SuggestedWordInfo>();
+ final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList();
if (puncs != null) {
for (final String puncSpec : puncs) {
- puncList.add(new SuggestedWords.SuggestedWordInfo(
- KeySpecParser.getLabel(puncSpec), SuggestedWordInfo.MAX_SCORE));
+ puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
+ SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED,
+ Dictionary.TYPE_HARDCODED));
}
}
return new SuggestedWords(puncList,
false /* typedWordValid */,
false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
true /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
false /* isPrediction */);
@@ -184,6 +213,16 @@ public class SettingsValues {
return wordSeparators;
}
+ private int createSuggestionVisibility(final Resources res) {
+ final String suggestionVisiblityStr = mShowSuggestionsSetting;
+ for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
+ if (suggestionVisiblityStr.equals(res.getString(visibility))) {
+ return visibility;
+ }
+ }
+ throw new RuntimeException("Bug: visibility string is not configured correctly");
+ }
+
private static boolean isVibrateOn(final Context context, final SharedPreferences prefs,
final Resources res) {
final boolean hasVibrator = VibratorUtils.getInstance(context).hasVibrator();
@@ -191,6 +230,22 @@ public class SettingsValues {
res.getBoolean(R.bool.config_default_vibration_enabled));
}
+ public boolean isApplicationSpecifiedCompletionsOn() {
+ return mInputAttributes.mApplicationSpecifiedCompletionOn;
+ }
+
+ public boolean isSuggestionsRequested(final int displayOrientation) {
+ return mInputAttributes.mIsSettingsSuggestionStripOn
+ && (mCorrectionEnabled
+ || isSuggestionStripVisibleInOrientation(displayOrientation));
+ }
+
+ public boolean isSuggestionStripVisibleInOrientation(final int orientation) {
+ return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE)
+ || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+ && orientation == Configuration.ORIENTATION_PORTRAIT);
+ }
+
public boolean isWordSeparator(int code) {
return mWordSeparators.contains(String.valueOf((char)code));
}
@@ -240,12 +295,6 @@ public class SettingsValues {
R.integer.config_key_preview_linger_timeout))));
}
- private static boolean isBigramSuggestionEnabled(final SharedPreferences sp,
- final Resources resources, final boolean autoCorrectEnabled) {
- // TODO: remove this method. Bigram suggestion is always true.
- return true;
- }
-
private static boolean isBigramPredictionEnabled(final SharedPreferences sp,
final Resources resources) {
return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
@@ -367,4 +416,13 @@ public class SettingsValues {
final String newStr = Utils.localeAndTimeHashMapToStr(map);
prefs.edit().putString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply();
}
+
+ public boolean isSameInputType(final EditorInfo editorInfo) {
+ return mInputAttributes.isSameInputType(editorInfo);
+ }
+
+ // For debug.
+ public String getInputAttributesDebugString() {
+ return mInputAttributes.toString();
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index a43b90525..39c59b44c 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -53,7 +53,7 @@ public class StringUtils {
if (TextUtils.isEmpty(csv)) return "";
final String[] elements = csv.split(",");
if (!containsInArray(key, elements)) return csv;
- final ArrayList<String> result = new ArrayList<String>(elements.length - 1);
+ final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1);
for (final String element : elements) {
if (!key.equals(element)) result.add(element);
}
@@ -184,6 +184,9 @@ public class StringUtils {
final char[] characters = string.toCharArray();
final int length = characters.length;
final int[] codePoints = new int[Character.codePointCount(characters, 0, length)];
+ if (length <= 0) {
+ return new int[0];
+ }
int codePoint = Character.codePointAt(characters, 0);
int dsti = 0;
for (int srci = Character.charCount(codePoint);
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
index 21c9c0d1e..de5f515b0 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -45,13 +45,13 @@ public class SubtypeLocale {
private static String[] sPredefinedKeyboardLayoutSet;
// Keyboard layout to its display name map.
private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap =
- new HashMap<String, String>();
+ CollectionUtils.newHashMap();
// Keyboard layout to subtype name resource id map.
private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap =
- new HashMap<String, Integer>();
+ CollectionUtils.newHashMap();
// Exceptional locale to subtype name resource id map.
private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap =
- new HashMap<String, Integer>();
+ CollectionUtils.newHashMap();
private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX =
"string/subtype_generic_";
private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX =
@@ -60,11 +60,11 @@ public class SubtypeLocale {
"string/subtype_no_language_";
// Exceptional locales to display name map.
private static final HashMap<String, String> sExceptionalDisplayNamesMap =
- new HashMap<String, String>();
+ CollectionUtils.newHashMap();
// Keyboard layout set name for the subtypes that don't have a keyboardLayoutSet extra value.
// This is for compatibility to keep the same subtype ids as pre-JellyBean.
private static final HashMap<String,String> sLocaleAndExtraValueToKeyboardLayoutSetMap =
- new HashMap<String,String>();
+ CollectionUtils.newHashMap();
private SubtypeLocale() {
// Intentional empty constructor for utility class.
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 664de6774..c693edcca 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.inputmethodservice.InputMethodService;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
@@ -42,7 +43,6 @@ public class SubtypeSwitcher {
private static final String TAG = SubtypeSwitcher.class.getSimpleName();
private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
- private /* final */ LatinIME mService;
private /* final */ InputMethodManager mImm;
private /* final */ Resources mResources;
private /* final */ ConnectivityManager mConnectivityManager;
@@ -68,11 +68,11 @@ public class SubtypeSwitcher {
return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage;
}
- public void updateEnabledSubtypeCount(int count) {
+ public void updateEnabledSubtypeCount(final int count) {
mEnabledSubtypeCount = count;
}
- public void updateIsSystemLanguageSameAsInputLanguage(boolean isSame) {
+ public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) {
mIsSystemLanguageSameAsInputLanguage = isSame;
}
}
@@ -81,26 +81,25 @@ public class SubtypeSwitcher {
return sInstance;
}
- public static void init(LatinIME service) {
- SubtypeLocale.init(service);
- sInstance.initialize(service);
- sInstance.updateAllParameters();
+ public static void init(final Context context) {
+ SubtypeLocale.init(context);
+ sInstance.initialize(context);
+ sInstance.updateAllParameters(context);
}
private SubtypeSwitcher() {
// Intentional empty constructor for singleton.
}
- private void initialize(LatinIME service) {
- mService = service;
+ private void initialize(final Context service) {
mResources = service.getResources();
mImm = ImfUtils.getInputMethodManager(service);
mConnectivityManager = (ConnectivityManager) service.getSystemService(
Context.CONNECTIVITY_SERVICE);
mCurrentSystemLocale = mResources.getConfiguration().locale;
- mCurrentSubtype = mImm.getCurrentInputMethodSubtype();
mNoLanguageSubtype = ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet(
service, SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
+ mCurrentSubtype = ImfUtils.getCurrentInputMethodSubtype(service, mNoLanguageSubtype);
if (mNoLanguageSubtype == null) {
throw new RuntimeException("Can't find no lanugage with QWERTY subtype");
}
@@ -111,39 +110,46 @@ public class SubtypeSwitcher {
// Update all parameters stored in SubtypeSwitcher.
// Only configuration changed event is allowed to call this because this is heavy.
- private void updateAllParameters() {
+ private void updateAllParameters(final Context context) {
mCurrentSystemLocale = mResources.getConfiguration().locale;
- updateSubtype(mImm.getCurrentInputMethodSubtype());
- updateParametersOnStartInputView();
+ updateSubtype(ImfUtils.getCurrentInputMethodSubtype(context, mNoLanguageSubtype));
+ updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled();
}
- // Update parameters which are changed outside LatinIME. This parameters affect UI so they
- // should be updated every time onStartInputview.
- public void updateParametersOnStartInputView() {
- updateEnabledSubtypes();
+ /**
+ * Update parameters which are changed outside LatinIME. This parameters affect UI so they
+ * should be updated every time onStartInputView.
+ *
+ * @return true if the current subtype is enabled.
+ */
+ public boolean updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled() {
+ final boolean currentSubtypeEnabled =
+ updateEnabledSubtypesAndReturnIfEnabled(mCurrentSubtype);
updateShortcutIME();
+ return currentSubtypeEnabled;
}
- // Reload enabledSubtypes from the framework.
- private void updateEnabledSubtypes() {
- final InputMethodSubtype currentSubtype = mCurrentSubtype;
- boolean foundCurrentSubtypeBecameDisabled = true;
+ /**
+ * Update enabled subtypes from the framework.
+ *
+ * @param subtype the subtype to be checked
+ * @return true if the {@code subtype} is enabled.
+ */
+ private boolean updateEnabledSubtypesAndReturnIfEnabled(final InputMethodSubtype subtype) {
final List<InputMethodSubtype> enabledSubtypesOfThisIme =
mImm.getEnabledInputMethodSubtypeList(null, true);
- for (InputMethodSubtype ims : enabledSubtypesOfThisIme) {
- if (ims.equals(currentSubtype)) {
- foundCurrentSubtypeBecameDisabled = false;
- }
- }
mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size());
- if (foundCurrentSubtypeBecameDisabled) {
- if (DBG) {
- Log.w(TAG, "Last subtype: "
- + currentSubtype.getLocale() + "/" + currentSubtype.getExtraValue());
- Log.w(TAG, "Last subtype was disabled. Update to the current one.");
+
+ for (final InputMethodSubtype ims : enabledSubtypesOfThisIme) {
+ if (ims.equals(subtype)) {
+ return true;
}
- updateSubtype(mImm.getCurrentInputMethodSubtype());
}
+ if (DBG) {
+ Log.w(TAG, "Subtype: " + subtype.getLocale() + "/" + subtype.getExtraValue()
+ + " was disabled");
+ }
+ return false;
}
private void updateShortcutIME() {
@@ -159,8 +165,8 @@ public class SubtypeSwitcher {
mImm.getShortcutInputMethodsAndSubtypes();
mShortcutInputMethodInfo = null;
mShortcutSubtype = null;
- for (InputMethodInfo imi : shortcuts.keySet()) {
- List<InputMethodSubtype> subtypes = shortcuts.get(imi);
+ for (final InputMethodInfo imi : shortcuts.keySet()) {
+ final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
// TODO: Returns the first found IMI for now. Should handle all shortcuts as
// appropriate.
mShortcutInputMethodInfo = imi;
@@ -194,24 +200,24 @@ public class SubtypeSwitcher {
mCurrentSubtype = newSubtype;
updateShortcutIME();
- mService.onRefreshKeyboard();
}
////////////////////////////
// Shortcut IME functions //
////////////////////////////
- public void switchToShortcutIME() {
+ public void switchToShortcutIME(final InputMethodService context) {
if (mShortcutInputMethodInfo == null) {
return;
}
final String imiId = mShortcutInputMethodInfo.getId();
- switchToTargetIME(imiId, mShortcutSubtype);
+ switchToTargetIME(imiId, mShortcutSubtype, context);
}
- private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype) {
- final IBinder token = mService.getWindow().getWindow().getAttributes().token;
+ private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
+ final InputMethodService context) {
+ final IBinder token = context.getWindow().getWindow().getAttributes().token;
if (token == null) {
return;
}
@@ -253,7 +259,7 @@ public class SubtypeSwitcher {
return true;
}
- public void onNetworkStateChanged(Intent intent) {
+ public void onNetworkStateChanged(final Intent intent) {
final boolean noConnection = intent.getBooleanExtra(
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
mIsNetworkConnected = !noConnection;
@@ -265,7 +271,7 @@ public class SubtypeSwitcher {
// Subtype Switching functions //
//////////////////////////////////
- public boolean needsToDisplayLanguage(Locale keyboardLocale) {
+ public boolean needsToDisplayLanguage(final Locale keyboardLocale) {
if (keyboardLocale.toString().equals(SubtypeLocale.NO_LANGUAGE)) {
return true;
}
@@ -279,12 +285,14 @@ public class SubtypeSwitcher {
return SubtypeLocale.getSubtypeLocale(mCurrentSubtype);
}
- public void onConfigurationChanged(Configuration conf) {
+ public boolean onConfigurationChanged(final Configuration conf, final Context context) {
final Locale systemLocale = conf.locale;
+ final boolean systemLocaleChanged = !systemLocale.equals(mCurrentSystemLocale);
// If system configuration was changed, update all parameters.
- if (!systemLocale.equals(mCurrentSystemLocale)) {
- updateAllParameters();
+ if (systemLocaleChanged) {
+ updateAllParameters(context);
}
+ return systemLocaleChanged;
}
public InputMethodSubtype getCurrentSubtype() {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 336a76f4b..51ed09604 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -18,7 +18,6 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.text.TextUtils;
-import android.util.Log;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.ProximityInfo;
@@ -26,6 +25,7 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import java.io.File;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
@@ -34,87 +34,50 @@ import java.util.concurrent.ConcurrentHashMap;
* 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 {
+public class Suggest {
public static final String TAG = Suggest.class.getSimpleName();
- public static final int APPROX_MAX_WORD_LENGTH = 32;
-
+ // TODO: rename this to CORRECTION_OFF
public static final int CORRECTION_NONE = 0;
+ // TODO: rename this to CORRECTION_ON
public static final int CORRECTION_FULL = 1;
- public static final int CORRECTION_FULL_BIGRAM = 2;
-
- // It seems the following values are only used for logging.
- public static final int DIC_USER_TYPED = 0;
- public static final int DIC_MAIN = 1;
- public static final int DIC_USER = 2;
- public static final int DIC_USER_HISTORY = 3;
- public static final int DIC_CONTACTS = 4;
- public static final int DIC_WHITELIST = 6;
- // If you add a type of dictionary, increment DIC_TYPE_LAST_ID
- // TODO: this value seems unused. Remove it?
- public static final int DIC_TYPE_LAST_ID = 6;
- public static final String DICT_KEY_MAIN = "main";
- public static final String DICT_KEY_CONTACTS = "contacts";
- // User dictionary, the system-managed one.
- public static final String DICT_KEY_USER = "user";
- // User history dictionary for the unigram map, internal to LatinIME
- public static final String DICT_KEY_USER_HISTORY_UNIGRAM = "history_unigram";
- // User history dictionary for the bigram map, internal to LatinIME
- public static final String DICT_KEY_USER_HISTORY_BIGRAM = "history_bigram";
- public static final String DICT_KEY_WHITELIST ="whitelist";
- private static final boolean DBG = LatinImeLogger.sDBG;
+ public interface SuggestInitializationListener {
+ public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
+ }
- private boolean mHasMainDictionary;
- private Dictionary mContactsDict;
- private WhitelistDictionary mWhiteListDictionary;
- private final ConcurrentHashMap<String, Dictionary> mUnigramDictionaries =
- new ConcurrentHashMap<String, Dictionary>();
- private final ConcurrentHashMap<String, Dictionary> mBigramDictionaries =
- new ConcurrentHashMap<String, Dictionary>();
+ private static final boolean DBG = LatinImeLogger.sDBG;
- private int mPrefMaxSuggestions = 18;
+ private Dictionary mMainDictionary;
+ private ContactsBinaryDictionary mContactsDict;
+ private final ConcurrentHashMap<String, Dictionary> mDictionaries =
+ CollectionUtils.newConcurrentHashMap();
- private static final int PREF_MAX_BIGRAMS = 60;
+ public static final int MAX_SUGGESTIONS = 18;
private float mAutoCorrectionThreshold;
- private ArrayList<SuggestedWordInfo> mSuggestions = new ArrayList<SuggestedWordInfo>();
- private ArrayList<SuggestedWordInfo> mBigramSuggestions = new ArrayList<SuggestedWordInfo>();
- private CharSequence mConsideredWord;
-
- // TODO: Remove these member variables by passing more context to addWord() callback method
- private boolean mIsFirstCharCapitalized;
- private boolean mIsAllUpperCase;
- private int mTrailingSingleQuotesCount;
-
- private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
+ // Locale used for upper- and title-casing words
+ private final Locale mLocale;
- public Suggest(final Context context, final Locale locale) {
- initAsynchronously(context, locale);
+ public Suggest(final Context context, final Locale locale,
+ final SuggestInitializationListener listener) {
+ initAsynchronously(context, locale, listener);
+ mLocale = locale;
}
/* package for test */ Suggest(final Context context, final File dictionary,
final long startOffset, final long length, final Locale locale) {
final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
startOffset, length /* useFullEditDistance */, false, locale);
- mHasMainDictionary = null != mainDict;
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
- initWhitelistAndAutocorrectAndPool(context, locale);
+ mLocale = locale;
+ mMainDictionary = mainDict;
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict);
}
- private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) {
- mWhiteListDictionary = new WhitelistDictionary(context, locale);
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary);
- }
-
- private void initAsynchronously(final Context context, final Locale locale) {
- resetMainDict(context, locale);
-
- // TODO: read the whitelist and init the pool asynchronously too.
- // initPool should be done asynchronously now that the pool is thread-safe.
- initWhitelistAndAutocorrectAndPool(context, locale);
+ private void initAsynchronously(final Context context, final Locale locale,
+ final SuggestInitializationListener listener) {
+ resetMainDict(context, locale, listener);
}
private static void addOrReplaceDictionary(
@@ -128,16 +91,22 @@ public class Suggest implements Dictionary.WordCallback {
}
}
- public void resetMainDict(final Context context, final Locale locale) {
- mHasMainDictionary = false;
+ public void resetMainDict(final Context context, final Locale locale,
+ final SuggestInitializationListener listener) {
+ mMainDictionary = null;
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+ }
new Thread("InitializeBinaryDictionary") {
@Override
public void run() {
final DictionaryCollection newMainDict =
DictionaryFactory.createMainDictionaryFromManager(context, locale);
- mHasMainDictionary = null != newMainDict && !newMainDict.isEmpty();
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, newMainDict);
+ mMainDictionary = newMainDict;
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+ }
}
}.start();
}
@@ -145,27 +114,27 @@ public class Suggest implements Dictionary.WordCallback {
// The main dictionary could have been loaded asynchronously. Don't cache the return value
// of this method.
public boolean hasMainDictionary() {
- return mHasMainDictionary;
+ return null != mMainDictionary && mMainDictionary.isInitialized();
}
- public Dictionary getContactsDictionary() {
- return mContactsDict;
+ public Dictionary getMainDictionary() {
+ return mMainDictionary;
}
- public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
- return mUnigramDictionaries;
+ public ContactsBinaryDictionary getContactsDictionary() {
+ return mContactsDict;
}
- public static int getApproxMaxWordLength() {
- return APPROX_MAX_WORD_LENGTH;
+ public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
+ return mDictionaries;
}
/**
* Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
* before the main dictionary, if set. This refers to the system-managed user dictionary.
*/
- public void setUserDictionary(Dictionary userDictionary) {
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary);
+ public void setUserDictionary(UserBinaryDictionary userDictionary) {
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER, userDictionary);
}
/**
@@ -173,236 +142,194 @@ public class Suggest implements Dictionary.WordCallback {
* the contacts dictionary by passing null to this method. In this case no contacts dictionary
* won't be used.
*/
- public void setContactsDictionary(Dictionary contactsDictionary) {
+ public void setContactsDictionary(ContactsBinaryDictionary contactsDictionary) {
mContactsDict = contactsDictionary;
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_CONTACTS, contactsDictionary);
}
- public void setUserHistoryDictionary(Dictionary userHistoryDictionary) {
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER_HISTORY_UNIGRAM,
- userHistoryDictionary);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_HISTORY_BIGRAM,
- userHistoryDictionary);
+ public void setUserHistoryDictionary(UserHistoryDictionary userHistoryDictionary) {
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
}
public void setAutoCorrectionThreshold(float threshold) {
mAutoCorrectionThreshold = threshold;
}
- private static CharSequence capitalizeWord(final boolean all, final boolean first,
- final CharSequence word) {
- if (TextUtils.isEmpty(word) || !(all || first)) return word;
- final int wordLength = word.length();
- final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
- // TODO: Must pay attention to locale when changing case.
- if (all) {
- sb.append(word.toString().toUpperCase());
- } else if (first) {
- sb.append(Character.toUpperCase(word.charAt(0)));
- if (wordLength > 1) {
- sb.append(word.subSequence(1, wordLength));
- }
- }
- return sb;
- }
-
- protected void addBigramToSuggestions(SuggestedWordInfo bigram) {
- mSuggestions.add(bigram);
+ public SuggestedWords getSuggestedWords(
+ final WordComposer wordComposer, CharSequence prevWordForBigram,
+ final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) {
+ return getSuggestedWordsWithSessionId(
+ wordComposer, prevWordForBigram, proximityInfo, isCorrectionEnabled, 0);
}
- private static final WordComposer sEmptyWordComposer = new WordComposer();
- public SuggestedWords getBigramPredictions(CharSequence prevWordForBigram) {
+ public SuggestedWords getSuggestedWordsWithSessionId(
+ final WordComposer wordComposer, CharSequence prevWordForBigram,
+ final ProximityInfo proximityInfo, final boolean isCorrectionEnabled, int sessionId) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
- mIsFirstCharCapitalized = false;
- mIsAllUpperCase = false;
- mTrailingSingleQuotesCount = 0;
- mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
-
- // Treating USER_TYPED as UNIGRAM suggestion for logging now.
- LatinImeLogger.onAddSuggestedWord("", Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
- mConsideredWord = "";
-
- mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
-
- getAllBigrams(prevWordForBigram, sEmptyWordComposer);
-
- // Nothing entered: return all bigrams for the previous word
- int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
- for (int i = 0; i < insertCount; ++i) {
- addBigramToSuggestions(mBigramSuggestions.get(i));
+ if (wordComposer.isBatchMode()) {
+ return getSuggestedWordsForBatchInput(
+ wordComposer, prevWordForBigram, proximityInfo, sessionId);
+ } else {
+ return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
+ isCorrectionEnabled);
}
-
- SuggestedWordInfo.removeDups(mSuggestions);
-
- return new SuggestedWords(mSuggestions,
- false /* typedWordValid */,
- false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
- false /* isPunctuationSuggestions */,
- false /* isObsoleteSuggestions */,
- true /* isPrediction */);
}
- // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
- public SuggestedWords getSuggestedWords(
+ // Retrieves suggestions for the typing input.
+ private SuggestedWords getSuggestedWordsForTypingInput(
final WordComposer wordComposer, CharSequence prevWordForBigram,
- final ProximityInfo proximityInfo, final int correctionMode) {
- LatinImeLogger.onStartSuggestion(prevWordForBigram);
- mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
- mIsAllUpperCase = wordComposer.isAllUpperCase();
- mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
- mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
+ final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) {
+ final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
+ final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
+ MAX_SUGGESTIONS);
final String typedWord = wordComposer.getTypedWord();
- final String consideredWord = mTrailingSingleQuotesCount > 0
- ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount)
+ final String consideredWord = trailingSingleQuotesCount > 0
+ ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount)
: typedWord;
- // Treating USER_TYPED as UNIGRAM suggestion for logging now.
- LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
- mConsideredWord = consideredWord;
-
- if (wordComposer.size() <= 1 && (correctionMode == CORRECTION_FULL_BIGRAM)) {
- // At first character typed, search only the bigrams
- mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
-
- if (!TextUtils.isEmpty(prevWordForBigram)) {
- getAllBigrams(prevWordForBigram, wordComposer);
- if (TextUtils.isEmpty(consideredWord)) {
- // Nothing entered: return all bigrams for the previous word
- int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
- for (int i = 0; i < insertCount; ++i) {
- addBigramToSuggestions(mBigramSuggestions.get(i));
- }
- } else {
- // Word entered: return only bigrams that match the first char of the typed word
- final char currentChar = consideredWord.charAt(0);
- // TODO: Must pay attention to locale when changing case.
- // TODO: Use codepoint instead of char
- final char currentCharUpper = Character.toUpperCase(currentChar);
- int count = 0;
- final int bigramSuggestionSize = mBigramSuggestions.size();
- for (int i = 0; i < bigramSuggestionSize; i++) {
- final SuggestedWordInfo bigramSuggestion = mBigramSuggestions.get(i);
- final char bigramSuggestionFirstChar =
- (char)bigramSuggestion.codePointAt(0);
- if (bigramSuggestionFirstChar == currentChar
- || bigramSuggestionFirstChar == currentCharUpper) {
- addBigramToSuggestions(bigramSuggestion);
- if (++count > mPrefMaxSuggestions) break;
- }
- }
- }
- }
+ LatinImeLogger.onAddSuggestedWord(typedWord, Dictionary.TYPE_USER_TYPED);
- } else if (wordComposer.size() > 1) {
- final WordComposer wordComposerForLookup;
- if (mTrailingSingleQuotesCount > 0) {
- wordComposerForLookup = new WordComposer(wordComposer);
- for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
- wordComposerForLookup.deleteLast();
- }
- } else {
- wordComposerForLookup = wordComposer;
- }
- // At second character typed, search the unigrams (scores being affected by bigrams)
- for (final String key : mUnigramDictionaries.keySet()) {
- // Skip UserUnigramDictionary and WhitelistDictionary to lookup
- if (key.equals(DICT_KEY_USER_HISTORY_UNIGRAM) || key.equals(DICT_KEY_WHITELIST))
- continue;
- final Dictionary dictionary = mUnigramDictionaries.get(key);
- dictionary.getWords(wordComposerForLookup, prevWordForBigram, this, proximityInfo);
+ final WordComposer wordComposerForLookup;
+ if (trailingSingleQuotesCount > 0) {
+ wordComposerForLookup = new WordComposer(wordComposer);
+ for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
+ wordComposerForLookup.deleteLast();
}
+ } else {
+ wordComposerForLookup = wordComposer;
}
- final CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase,
- mIsFirstCharCapitalized, mWhiteListDictionary.getWhitelistedWord(consideredWord));
+ for (final String key : mDictionaries.keySet()) {
+ final Dictionary dictionary = mDictionaries.get(key);
+ suggestionsSet.addAll(dictionary.getSuggestions(
+ wordComposerForLookup, prevWordForBigram, proximityInfo));
+ }
- final boolean hasAutoCorrection;
- if (CORRECTION_FULL == correctionMode || CORRECTION_FULL_BIGRAM == correctionMode) {
- final CharSequence autoCorrection =
- AutoCorrection.computeAutoCorrectionWord(mUnigramDictionaries, wordComposer,
- mSuggestions, consideredWord, mAutoCorrectionThreshold,
- whitelistedWord);
- hasAutoCorrection = (null != autoCorrection);
+ final CharSequence whitelistedWord;
+ if (suggestionsSet.isEmpty()) {
+ whitelistedWord = null;
+ } else if (SuggestedWordInfo.KIND_WHITELIST != suggestionsSet.first().mKind) {
+ whitelistedWord = null;
} else {
+ whitelistedWord = suggestionsSet.first().mWord;
+ }
+
+ final boolean allowsToBeAutoCorrected = (null != whitelistedWord
+ && !whitelistedWord.equals(consideredWord))
+ || AutoCorrection.isNotAWord(mDictionaries, consideredWord,
+ wordComposer.isFirstCharCapitalized());
+
+ final boolean hasAutoCorrection;
+ // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because
+ // any attempt to do auto-correction is already shielded with a test for this flag; at the
+ // same time, it feels wrong that the SuggestedWord object includes information about
+ // the current settings. It may also be useful to know, when the setting is off, whether
+ // the word *would* have been auto-corrected.
+ if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord()
+ || suggestionsSet.isEmpty() || wordComposer.hasDigits()
+ || wordComposer.isMostlyCaps() || wordComposer.isResumed()
+ || !hasMainDictionary()) {
+ // If we don't have a main dictionary, we never want to auto-correct. The reason for
+ // this is, the user may have a contact whose name happens to match a valid word in
+ // their language, and it will unexpectedly auto-correct. For example, if the user
+ // types in English with no dictionary and has a "Will" in their contact list, "will"
+ // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no
+ // auto-correct.
hasAutoCorrection = false;
+ } else {
+ hasAutoCorrection = AutoCorrection.suggestionExceedsAutoCorrectionThreshold(
+ suggestionsSet.first(), consideredWord, mAutoCorrectionThreshold);
}
- if (whitelistedWord != null) {
- if (mTrailingSingleQuotesCount > 0) {
- final StringBuilder sb = new StringBuilder(whitelistedWord);
- for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
- sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
- }
- mSuggestions.add(0, new SuggestedWordInfo(
- sb.toString(), SuggestedWordInfo.MAX_SCORE));
- } else {
- mSuggestions.add(0, new SuggestedWordInfo(
- whitelistedWord, SuggestedWordInfo.MAX_SCORE));
+ final ArrayList<SuggestedWordInfo> suggestionsContainer =
+ CollectionUtils.newArrayList(suggestionsSet);
+ final int suggestionsCount = suggestionsContainer.size();
+ final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
+ final boolean isAllUpperCase = wordComposer.isAllUpperCase();
+ if (isFirstCharCapitalized || isAllUpperCase || 0 != trailingSingleQuotesCount) {
+ for (int i = 0; i < suggestionsCount; ++i) {
+ final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
+ wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized,
+ trailingSingleQuotesCount);
+ suggestionsContainer.set(i, transformedWordInfo);
}
}
- mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
- SuggestedWordInfo.removeDups(mSuggestions);
+ for (int i = 0; i < suggestionsCount; ++i) {
+ final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict);
+ }
+
+ if (!TextUtils.isEmpty(typedWord)) {
+ suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
+ SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
+ Dictionary.TYPE_USER_TYPED));
+ }
+ SuggestedWordInfo.removeDups(suggestionsContainer);
final ArrayList<SuggestedWordInfo> suggestionsList;
- if (DBG) {
- suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, mSuggestions);
+ if (DBG && !suggestionsContainer.isEmpty()) {
+ suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, suggestionsContainer);
} else {
- suggestionsList = mSuggestions;
+ suggestionsList = suggestionsContainer;
}
- // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
- // but still autocorrected from - in the case the whitelist only capitalizes the word.
- // The whitelist should be case-insensitive, so it's not possible to be consistent with
- // a boolean flag. Right now this is handled with a slight hack in
- // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
- final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
- getUnigramDictionaries(), consideredWord, wordComposer.isFirstCharCapitalized())
- // If we don't have a main dictionary, we never want to auto-correct. The reason for this
- // is, the user may have a contact whose name happens to match a valid word in their
- // language, and it will unexpectedly auto-correct. For example, if the user types in
- // English with no dictionary and has a "Will" in their contact list, "will" would
- // always auto-correct to "Will" which is unwanted. Hence, no main dict => no auto-correct.
- && mHasMainDictionary;
-
- boolean autoCorrectionAvailable = hasAutoCorrection;
- if (correctionMode == CORRECTION_FULL || correctionMode == CORRECTION_FULL_BIGRAM) {
- autoCorrectionAvailable |= !allowsToBeAutoCorrected;
- }
- // Don't auto-correct words with multiple capital letter
- autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
- autoCorrectionAvailable &= !wordComposer.isResumed();
- if (allowsToBeAutoCorrected && suggestionsList.size() > 1 && mAutoCorrectionThreshold > 0
- && Suggest.shouldBlockAutoCorrectionBySafetyNet(typedWord,
- suggestionsList.get(1).mWord)) {
- autoCorrectionAvailable = false;
- }
return new SuggestedWords(suggestionsList,
+ // TODO: this first argument is lying. If this is a whitelisted word which is an
+ // actual word, it says typedWordValid = false, which looks wrong. We should either
+ // rename the attribute or change the value.
!allowsToBeAutoCorrected /* typedWordValid */,
- autoCorrectionAvailable /* hasAutoCorrectionCandidate */,
- allowsToBeAutoCorrected /* allowsToBeAutoCorrected */,
+ hasAutoCorrection, /* willAutoCorrect */
false /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
- false /* isPrediction */);
+ !wordComposer.isComposingWord() /* isPrediction */);
}
- /**
- * Adds all bigram predictions for prevWord. Also checks the lower case version of prevWord if
- * it contains any upper case characters.
- */
- private void getAllBigrams(final CharSequence prevWord, final WordComposer wordComposer) {
- if (StringUtils.hasUpperCase(prevWord)) {
- // TODO: Must pay attention to locale when changing case.
- final CharSequence lowerPrevWord = prevWord.toString().toLowerCase();
- for (final Dictionary dictionary : mBigramDictionaries.values()) {
- dictionary.getBigrams(wordComposer, lowerPrevWord, this);
+ // Retrieves suggestions for the batch input.
+ private SuggestedWords getSuggestedWordsForBatchInput(
+ final WordComposer wordComposer, CharSequence prevWordForBigram,
+ final ProximityInfo proximityInfo, int sessionId) {
+ final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
+ MAX_SUGGESTIONS);
+
+ // At second character typed, search the unigrams (scores being affected by bigrams)
+ for (final String key : mDictionaries.keySet()) {
+ // Skip User history dictionary for lookup
+ // TODO: The user history dictionary should just override getSuggestionsWithSessionId
+ // to make sure it doesn't return anything and we should remove this test
+ if (key.equals(Dictionary.TYPE_USER_HISTORY)) {
+ continue;
}
+ final Dictionary dictionary = mDictionaries.get(key);
+ suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(
+ wordComposer, prevWordForBigram, proximityInfo, sessionId));
}
- for (final Dictionary dictionary : mBigramDictionaries.values()) {
- dictionary.getBigrams(wordComposer, prevWord, this);
+
+ final ArrayList<SuggestedWordInfo> suggestionsContainer =
+ CollectionUtils.newArrayList(suggestionsSet);
+ final int suggestionsCount = suggestionsContainer.size();
+ final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock();
+ final boolean isAllUpperCase = wordComposer.isAllUpperCase();
+ if (isFirstCharCapitalized || isAllUpperCase) {
+ for (int i = 0; i < suggestionsCount; ++i) {
+ final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
+ wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized,
+ 0 /* trailingSingleQuotesCount */);
+ suggestionsContainer.set(i, transformedWordInfo);
+ }
}
+
+ SuggestedWordInfo.removeDups(suggestionsContainer);
+ // In the batch input mode, the most relevant suggested word should act as a "typed word"
+ // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
+ return new SuggestedWords(suggestionsContainer,
+ true /* typedWordValid */,
+ false /* willAutoCorrect */,
+ false /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
}
private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
@@ -411,7 +338,7 @@ public class Suggest implements Dictionary.WordCallback {
typedWordInfo.setDebugString("+");
final int suggestionsSize = suggestions.size();
final ArrayList<SuggestedWordInfo> suggestionsList =
- new ArrayList<SuggestedWordInfo>(suggestionsSize);
+ CollectionUtils.newArrayList(suggestionsSize);
suggestionsList.add(typedWordInfo);
// Note: i here is the index in mScores[], but the index in mSuggestions is one more
// than i because we added the typed word to mSuggestions without touching mScores.
@@ -431,119 +358,44 @@ public class Suggest implements Dictionary.WordCallback {
return suggestionsList;
}
- // TODO: Use codepoint instead of char
- @Override
- public boolean addWord(final char[] word, final int offset, final int length, int score,
- final int dicTypeId, final int dataType) {
- int dataTypeForLog = dataType;
- final ArrayList<SuggestedWordInfo> suggestions;
- final int prefMaxSuggestions;
- if (dataType == Dictionary.BIGRAM) {
- suggestions = mBigramSuggestions;
- prefMaxSuggestions = PREF_MAX_BIGRAMS;
- } else {
- suggestions = mSuggestions;
- prefMaxSuggestions = mPrefMaxSuggestions;
+ private static class SuggestedWordInfoComparator implements Comparator<SuggestedWordInfo> {
+ // This comparator ranks the word info with the higher frequency first. That's because
+ // that's the order we want our elements in.
+ @Override
+ public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) {
+ if (o1.mScore > o2.mScore) return -1;
+ if (o1.mScore < o2.mScore) return 1;
+ if (o1.mCodePointCount < o2.mCodePointCount) return -1;
+ if (o1.mCodePointCount > o2.mCodePointCount) return 1;
+ return o1.mWord.toString().compareTo(o2.mWord.toString());
}
-
- int pos = 0;
-
- // Check if it's the same word, only caps are different
- if (StringUtils.equalsIgnoreCase(mConsideredWord, word, offset, length)) {
- // TODO: remove this surrounding if clause and move this logic to
- // getSuggestedWordBuilder.
- if (suggestions.size() > 0) {
- final SuggestedWordInfo currentHighestWord = suggestions.get(0);
- // If the current highest word is also equal to typed word, we need to compare
- // frequency to determine the insertion position. This does not ensure strictly
- // correct ordering, but ensures the top score is on top which is enough for
- // removing duplicates correctly.
- if (StringUtils.equalsIgnoreCase(currentHighestWord.mWord, word, offset, length)
- && score <= currentHighestWord.mScore) {
- pos = 1;
- }
- }
- } else {
- // Check the last one's score and bail
- if (suggestions.size() >= prefMaxSuggestions
- && suggestions.get(prefMaxSuggestions - 1).mScore >= score) return true;
- while (pos < suggestions.size()) {
- final int curScore = suggestions.get(pos).mScore;
- if (curScore < score
- || (curScore == score && length < suggestions.get(pos).codePointCount())) {
- break;
- }
- pos++;
- }
- }
- if (pos >= prefMaxSuggestions) {
- return true;
- }
-
- final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
- // TODO: Must pay attention to locale when changing case.
- 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);
- }
+ }
+ private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator =
+ new SuggestedWordInfoComparator();
+
+ private static SuggestedWordInfo getTransformedSuggestedWordInfo(
+ final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
+ final boolean isFirstCharCapitalized, final int trailingSingleQuotesCount) {
+ final StringBuilder sb = new StringBuilder(wordInfo.mWord.length());
+ if (isAllUpperCase) {
+ sb.append(wordInfo.mWord.toString().toUpperCase(locale));
+ } else if (isFirstCharCapitalized) {
+ sb.append(StringUtils.toTitleCase(wordInfo.mWord.toString(), locale));
} else {
- sb.append(word, offset, length);
+ sb.append(wordInfo.mWord);
}
- for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
+ for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
}
- suggestions.add(pos, new SuggestedWordInfo(sb, score));
- if (suggestions.size() > prefMaxSuggestions) {
- suggestions.remove(prefMaxSuggestions);
- } else {
- LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
- }
- return true;
+ return new SuggestedWordInfo(sb, wordInfo.mScore, wordInfo.mKind, wordInfo.mSourceDict);
}
public void close() {
- final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>();
- dictionaries.addAll(mUnigramDictionaries.values());
- dictionaries.addAll(mBigramDictionaries.values());
+ final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
+ dictionaries.addAll(mDictionaries.values());
for (final Dictionary dictionary : dictionaries) {
dictionary.close();
}
- mHasMainDictionary = false;
- }
-
- // TODO: Resolve the inconsistencies between the native auto correction algorithms and
- // this safety net
- public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
- final CharSequence suggestion) {
- // Safety net for auto correction.
- // Actually if we hit this safety net, it's a bug.
- // If user selected aggressive auto correction mode, there is no need to use the safety
- // net.
- // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
- // we should not use net because relatively edit distance can be big.
- final int typedWordLength = typedWord.length();
- if (typedWordLength < Suggest.MINIMUM_SAFETY_NET_CHAR_LENGTH) {
- return false;
- }
- final int maxEditDistanceOfNativeDictionary =
- (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
- final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
- if (DBG) {
- Log.d(TAG, "Autocorrected edit distance = " + distance
- + ", " + maxEditDistanceOfNativeDictionary);
- }
- if (distance > maxEditDistanceOfNativeDictionary) {
- if (DBG) {
- Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
- Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
- + "Turning off auto-correction.");
- }
- return true;
- } else {
- return false;
- }
+ mMainDictionary = null;
}
}
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 497fd3bfa..68ecfa0d7 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -24,28 +24,30 @@ import java.util.Arrays;
import java.util.HashSet;
public class SuggestedWords {
+ private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST =
+ CollectionUtils.newArrayList(0);
public static final SuggestedWords EMPTY = new SuggestedWords(
- new ArrayList<SuggestedWordInfo>(0), false, false, false, false, false, false);
+ EMPTY_WORD_INFO_LIST, false, false, false, false, false);
public final boolean mTypedWordValid;
- public final boolean mHasAutoCorrectionCandidate;
+ // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition
+ // of what this flag means would be "the top suggestion is strong enough to auto-correct",
+ // whether this exactly matches the user entry or not.
+ public final boolean mWillAutoCorrect;
public final boolean mIsPunctuationSuggestions;
- public final boolean mAllowsToBeAutoCorrected;
public final boolean mIsObsoleteSuggestions;
public final boolean mIsPrediction;
private final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList;
public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
final boolean typedWordValid,
- final boolean hasAutoCorrectionCandidate,
- final boolean allowsToBeAutoCorrected,
+ final boolean willAutoCorrect,
final boolean isPunctuationSuggestions,
final boolean isObsoleteSuggestions,
final boolean isPrediction) {
mSuggestedWordInfoList = suggestedWordInfoList;
mTypedWordValid = typedWordValid;
- mHasAutoCorrectionCandidate = hasAutoCorrectionCandidate;
- mAllowsToBeAutoCorrected = allowsToBeAutoCorrected;
+ mWillAutoCorrect = willAutoCorrect;
mIsPunctuationSuggestions = isPunctuationSuggestions;
mIsObsoleteSuggestions = isObsoleteSuggestions;
mIsPrediction = isPrediction;
@@ -55,7 +57,7 @@ public class SuggestedWords {
return mSuggestedWordInfoList.size();
}
- public CharSequence getWord(int pos) {
+ public String getWord(int pos) {
return mSuggestedWordInfoList.get(pos).mWord;
}
@@ -67,12 +69,8 @@ public class SuggestedWords {
return mSuggestedWordInfoList.get(pos);
}
- public boolean hasAutoCorrectionWord() {
- return mHasAutoCorrectionCandidate && size() > 1 && !mTypedWordValid;
- }
-
public boolean willAutoCorrect() {
- return !mTypedWordValid && mHasAutoCorrectionCandidate;
+ return mWillAutoCorrect;
}
@Override
@@ -80,18 +78,18 @@ public class SuggestedWords {
// Pretty-print method to help debug
return "SuggestedWords:"
+ " mTypedWordValid=" + mTypedWordValid
- + " mHasAutoCorrectionCandidate=" + mHasAutoCorrectionCandidate
- + " mAllowsToBeAutoCorrected=" + mAllowsToBeAutoCorrected
+ + " mWillAutoCorrect=" + mWillAutoCorrect
+ " mIsPunctuationSuggestions=" + mIsPunctuationSuggestions
+ " words=" + Arrays.toString(mSuggestedWordInfoList.toArray());
}
public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions(
final CompletionInfo[] infos) {
- final ArrayList<SuggestedWordInfo> result = new ArrayList<SuggestedWordInfo>();
+ final ArrayList<SuggestedWordInfo> result = CollectionUtils.newArrayList();
for (CompletionInfo info : infos) {
if (null != info && info.getText() != null) {
- result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE));
+ result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_APP_DEFINED, Dictionary.TYPE_APPLICATION_DEFINED));
}
}
return result;
@@ -101,9 +99,10 @@ public class SuggestedWords {
// and replace it with what the user currently typed.
public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
final CharSequence typedWord, final SuggestedWords previousSuggestions) {
- final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<SuggestedWordInfo>();
- final HashSet<String> alreadySeen = new HashSet<String>();
- suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
+ final ArrayList<SuggestedWordInfo> suggestionsList = CollectionUtils.newArrayList();
+ final HashSet<String> alreadySeen = CollectionUtils.newHashSet();
+ suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_USER_TYPED));
alreadySeen.add(typedWord.toString());
final int previousSize = previousSuggestions.size();
for (int pos = 1; pos < previousSize; pos++) {
@@ -120,17 +119,29 @@ public class SuggestedWords {
public static class SuggestedWordInfo {
public static final int MAX_SCORE = Integer.MAX_VALUE;
- private final String mWordStr;
- public final CharSequence mWord;
+ public static final int KIND_TYPED = 0; // What user typed
+ public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
+ public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
+ public static final int KIND_WHITELIST = 3; // Whitelisted word
+ public static final int KIND_BLACKLIST = 4; // Blacklisted word
+ public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
+ public static final int KIND_APP_DEFINED = 6; // Suggested by the application
+ public static final int KIND_SHORTCUT = 7; // A shortcut
+ public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
+ public final String mWord;
public final int mScore;
+ public final int mKind; // one of the KIND_* constants above
public final int mCodePointCount;
+ public final String mSourceDict;
private String mDebugString = "";
- public SuggestedWordInfo(final CharSequence word, final int score) {
- mWordStr = word.toString();
- mWord = word;
+ public SuggestedWordInfo(final CharSequence word, final int score, final int kind,
+ final String sourceDict) {
+ mWord = word.toString();
mScore = score;
- mCodePointCount = mWordStr.codePointCount(0, mWordStr.length());
+ mKind = kind;
+ mSourceDict = sourceDict;
+ mCodePointCount = StringUtils.codePointCount(mWord);
}
@@ -148,15 +159,15 @@ public class SuggestedWords {
}
public int codePointAt(int i) {
- return mWordStr.codePointAt(i);
+ return mWord.codePointAt(i);
}
@Override
public String toString() {
if (TextUtils.isEmpty(mDebugString)) {
- return mWordStr;
+ return mWord;
} else {
- return mWordStr + " (" + mDebugString.toString() + ")";
+ return mWord + " (" + mDebugString.toString() + ")";
}
}
@@ -170,7 +181,7 @@ public class SuggestedWords {
final SuggestedWordInfo cur = candidates.get(i);
for (int j = 0; j < i; ++j) {
final SuggestedWordInfo previous = candidates.get(j);
- if (TextUtils.equals(cur.mWord, previous.mWord)) {
+ if (cur.mWord.equals(previous.mWord)) {
candidates.remove(cur.mScore < previous.mScore ? i : j);
--i;
break;
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index 673b54500..bdd988df2 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -19,22 +19,23 @@ package com.android.inputmethod.latin;
import android.content.Context;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import java.util.ArrayList;
import java.util.Locale;
public class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary {
private boolean mClosed;
public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) {
- super(context, Suggest.DIC_CONTACTS, locale);
+ super(context, locale);
}
@Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
+ public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
syncReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
deleted file mode 100644
index a8b871cdf..000000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * 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.android.inputmethod.latin;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedContactsDictionary extends ContactsDictionary {
- private boolean mClosed;
-
- public SynchronouslyLoadedContactsDictionary(final Context context) {
- super(context, Suggest.DIC_CONTACTS);
- mClosed = false;
- }
-
- @Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- blockingReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- blockingReloadDictionaryIfRequired();
- return getWordFrequency(word) > -1;
- }
-
- // Protect against multiple closing
- @Override
- public synchronized void close() {
- // Actually with the current implementation of ContactsDictionary it's safe to close
- // several times, so the following protection is really only for foolproofing
- if (mClosed) return;
- mClosed = true;
- super.close();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 1606a34e0..b8cfddd4e 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -19,6 +19,9 @@ package com.android.inputmethod.latin;
import android.content.Context;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.ArrayList;
public class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary {
@@ -32,11 +35,10 @@ public class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionar
}
@Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
+ public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
syncReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
deleted file mode 100644
index 23a49c192..000000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * 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.android.inputmethod.latin;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedUserDictionary extends UserDictionary {
- private boolean mClosed;
-
- public SynchronouslyLoadedUserDictionary(final Context context, final String locale) {
- this(context, locale, false);
- }
-
- public SynchronouslyLoadedUserDictionary(final Context context, final String locale,
- final boolean alsoUseMoreRestrictiveLocales) {
- super(context, locale, alsoUseMoreRestrictiveLocales);
- }
-
- @Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- blockingReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- blockingReloadDictionaryIfRequired();
- return super.isValidWord(word);
- }
-
- // Protect against multiple closing
- @Override
- public synchronized void close() {
- if (mClosed) return;
- mClosed = true;
- super.close();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 6fa1a25a1..60e6fa127 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -34,13 +34,27 @@ import java.util.Arrays;
*/
public class UserBinaryDictionary extends ExpandableBinaryDictionary {
- // TODO: use Words.SHORTCUT when it's public in the SDK
+ // The user dictionary provider uses an empty string to mean "all languages".
+ private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
+
+ // TODO: use Words.SHORTCUT when we target JellyBean or above
final static String SHORTCUT = "shortcut";
- private static final String[] PROJECTION_QUERY = {
- Words.WORD,
- SHORTCUT,
- Words.FREQUENCY,
- };
+ private static final String[] PROJECTION_QUERY;
+ static {
+ // 16 is JellyBean, but we want this to compile against ICS.
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+ PROJECTION_QUERY = new String[] {
+ Words.WORD,
+ SHORTCUT,
+ Words.FREQUENCY,
+ };
+ } else {
+ PROJECTION_QUERY = new String[] {
+ Words.WORD,
+ Words.FREQUENCY,
+ };
+ }
+ }
private static final String NAME = "userunigram";
@@ -58,9 +72,14 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
public UserBinaryDictionary(final Context context, final String locale,
final boolean alsoUseMoreRestrictiveLocales) {
- super(context, getFilenameWithLocale(NAME, locale), Suggest.DIC_USER);
+ super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER);
if (null == locale) throw new NullPointerException(); // Catch the error earlier
- mLocale = locale;
+ if (SubtypeLocale.NO_LANGUAGE.equals(locale)) {
+ // If we don't have a locale, insert into the "all locales" user dictionary.
+ mLocale = USER_DICTIONARY_ALL_LANGUAGES;
+ } else {
+ mLocale = locale;
+ }
mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
// Perform a managed query. The Activity will handle closing and re-querying the cursor
// when needed.
@@ -136,7 +155,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
requestArguments = localeElements;
}
final Cursor cursor = mContext.getContentResolver().query(
- Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
+ Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
try {
addWords(cursor);
} finally {
@@ -182,16 +201,18 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
}
private void addWords(Cursor cursor) {
+ // 16 is JellyBean, but we want this to compile against ICS.
+ final boolean hasShortcutColumn = android.os.Build.VERSION.SDK_INT >= 16;
clearFusionDictionary();
if (cursor == null) return;
if (cursor.moveToFirst()) {
final int indexWord = cursor.getColumnIndex(Words.WORD);
- final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
+ final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(SHORTCUT) : 0;
final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
while (!cursor.isAfterLast()) {
- String word = cursor.getString(indexWord);
- String shortcut = cursor.getString(indexShortcut);
- int frequency = cursor.getInt(indexFrequency);
+ final String word = cursor.getString(indexWord);
+ final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null;
+ final int frequency = cursor.getInt(indexFrequency);
// Safeguard against adding really long words.
if (word.length() < MAX_WORD_LENGTH) {
super.addWord(word, null, frequency);
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
deleted file mode 100644
index c1efadd44..000000000
--- a/java/src/com/android/inputmethod/latin/UserDictionary.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * 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.android.inputmethod.latin;
-
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.provider.UserDictionary.Words;
-import android.text.TextUtils;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-import java.util.Arrays;
-
-// TODO: This class is superseded by {@link UserBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words in the user unigram dictionary.
- *
- * @deprecated Use {@link UserBinaryDictionary}.
- */
-@Deprecated
-public class UserDictionary extends ExpandableDictionary {
-
- // TODO: use Words.SHORTCUT when it's public in the SDK
- final static String SHORTCUT = "shortcut";
- private static final String[] PROJECTION_QUERY = {
- Words.WORD,
- SHORTCUT,
- Words.FREQUENCY,
- };
-
- // This is not exported by the framework so we pretty much have to write it here verbatim
- private static final String ACTION_USER_DICTIONARY_INSERT =
- "com.android.settings.USER_DICTIONARY_INSERT";
-
- private ContentObserver mObserver;
- final private String mLocale;
- final private boolean mAlsoUseMoreRestrictiveLocales;
-
- public UserDictionary(final Context context, final String locale) {
- this(context, locale, false);
- }
-
- public UserDictionary(final Context context, final String locale,
- final boolean alsoUseMoreRestrictiveLocales) {
- super(context, Suggest.DIC_USER);
- if (null == locale) throw new NullPointerException(); // Catch the error earlier
- mLocale = locale;
- mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
- // Perform a managed query. The Activity will handle closing and re-querying the cursor
- // when needed.
- ContentResolver cres = context.getContentResolver();
-
- mObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean self) {
- setRequiresReload(true);
- }
- };
- cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
-
- loadDictionary();
- }
-
- @Override
- public synchronized void close() {
- if (mObserver != null) {
- getContext().getContentResolver().unregisterContentObserver(mObserver);
- mObserver = null;
- }
- super.close();
- }
-
- @Override
- public void loadDictionaryAsync() {
- // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
- // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
- // This is correct for locale processing.
- // For this example, we'll look at the "en_US_POSIX" case.
- final String[] localeElements =
- TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3);
- final int length = localeElements.length;
-
- final StringBuilder request = new StringBuilder("(locale is NULL)");
- String localeSoFar = "";
- // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
- // and request = "(locale is NULL)"
- for (int i = 0; i < length; ++i) {
- // i | localeSoFar | localeElements
- // 0 | "" | ["en", "US", "POSIX"]
- // 1 | "en_" | ["en", "US", "POSIX"]
- // 2 | "en_US_" | ["en", "en_US", "POSIX"]
- localeElements[i] = localeSoFar + localeElements[i];
- localeSoFar = localeElements[i] + "_";
- // i | request
- // 0 | "(locale is NULL)"
- // 1 | "(locale is NULL) or (locale=?)"
- // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
- request.append(" or (locale=?)");
- }
- // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
- // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
-
- final String[] requestArguments;
- // If length == 3, we already have all the arguments we need (common prefix is meaningless
- // inside variants
- if (mAlsoUseMoreRestrictiveLocales && length < 3) {
- request.append(" or (locale like ?)");
- // The following creates an array with one more (null) position
- final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
- Arrays.copyOf(localeElements, length + 1);
- localeElementsWithMoreRestrictiveLocalesIncluded[length] =
- localeElements[length - 1] + "_%";
- requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
- // If for example localeElements = ["en"]
- // then requestArguments = ["en", "en_%"]
- // and request = (locale is NULL) or (locale=?) or (locale like ?)
- // If localeElements = ["en", "en_US"]
- // then requestArguments = ["en", "en_US", "en_US_%"]
- } else {
- requestArguments = localeElements;
- }
- final Cursor cursor = getContext().getContentResolver()
- .query(Words.CONTENT_URI, PROJECTION_QUERY, request.toString(),
- requestArguments, null);
- try {
- addWords(cursor);
- } finally {
- if (null != cursor) cursor.close();
- }
- }
-
- public boolean isEnabled() {
- final ContentResolver cr = getContext().getContentResolver();
- final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
- if (client != null) {
- client.release();
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Adds a word to the user dictionary and makes it persistent.
- *
- * This will call upon the system interface to do the actual work through the intent
- * readied by the system to this effect.
- *
- * @param word the word to add. If the word is capitalized, then the dictionary will
- * recognize it as a capitalized word when searched.
- * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
- * the highest.
- * @TODO use a higher or float range for frequency
- */
- public synchronized void addWordToUserDictionary(final String word, final int frequency) {
- // Force load the dictionary here synchronously
- if (getRequiresReload()) loadDictionaryAsync();
- // TODO: do something for the UI. With the following, any sufficiently long word will
- // look like it will go to the user dictionary but it won't.
- // Safeguard against adding long words. Can cause stack overflow.
- if (word.length() >= getMaxWordLength()) return;
-
- // TODO: Add an argument to the intent to specify the frequency.
- Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT);
- intent.putExtra(Words.WORD, word);
- intent.putExtra(Words.LOCALE, mLocale);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- getContext().startActivity(intent);
- }
-
- @Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- super.getWords(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- return super.isValidWord(word);
- }
-
- private void addWords(Cursor cursor) {
- clearDictionary();
- if (cursor == null) return;
- final int maxWordLength = getMaxWordLength();
- if (cursor.moveToFirst()) {
- final int indexWord = cursor.getColumnIndex(Words.WORD);
- final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
- final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
- while (!cursor.isAfterLast()) {
- String word = cursor.getString(indexWord);
- String shortcut = cursor.getString(indexShortcut);
- int frequency = cursor.getInt(indexFrequency);
- // Safeguard against adding really long words. Stack may overflow due
- // to recursion
- if (word.length() < maxWordLength) {
- super.addWord(word, null, frequency);
- }
- if (null != shortcut && shortcut.length() < maxWordLength) {
- super.addWord(shortcut, word, frequency);
- }
- cursor.moveToNext();
- }
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
index 5095f6582..6c9d1c250 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
@@ -27,9 +27,12 @@ import android.os.AsyncTask;
import android.provider.BaseColumns;
import android.util.Log;
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
import java.lang.ref.SoftReference;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@@ -49,14 +52,14 @@ public class UserHistoryDictionary extends ExpandableDictionary {
private static final int FREQUENCY_FOR_TYPED = 2;
/** Maximum number of pairs. Pruning will start when databases goes above this number. */
- private static int sMaxHistoryBigrams = 10000;
+ public static final int sMaxHistoryBigrams = 10000;
/**
* When it hits maximum bigram pair, it will delete until you are left with
* only (sMaxHistoryBigrams - sDeleteHistoryBigrams) pairs.
* Do not keep this number small to avoid deleting too often.
*/
- private static int sDeleteHistoryBigrams = 1000;
+ public static final int sDeleteHistoryBigrams = 1000;
/**
* Database version should increase if the database structure changes
@@ -90,10 +93,10 @@ public class UserHistoryDictionary extends ExpandableDictionary {
private final static HashMap<String, String> sDictProjectionMap;
private final static ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
- sLangDictCache = new ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>();
+ sLangDictCache = CollectionUtils.newConcurrentHashMap();
static {
- sDictProjectionMap = new HashMap<String, String>();
+ sDictProjectionMap = CollectionUtils.newHashMap();
sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID);
sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1);
sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2);
@@ -106,17 +109,12 @@ public class UserHistoryDictionary extends ExpandableDictionary {
private static DatabaseHelper sOpenHelper = null;
- public void setDatabaseMax(int maxHistoryBigram) {
- sMaxHistoryBigrams = maxHistoryBigram;
- }
-
- public void setDatabaseDelete(int deleteHistoryBigram) {
- sDeleteHistoryBigrams = deleteHistoryBigram;
+ public String getLocale() {
+ return mLocale;
}
public synchronized static UserHistoryDictionary getInstance(
- final Context context, final String locale,
- final int dictTypeId, final SharedPreferences sp) {
+ final Context context, final String locale, final SharedPreferences sp) {
if (sLangDictCache.containsKey(locale)) {
final SoftReference<UserHistoryDictionary> ref = sLangDictCache.get(locale);
final UserHistoryDictionary dict = ref == null ? null : ref.get();
@@ -128,14 +126,14 @@ public class UserHistoryDictionary extends ExpandableDictionary {
}
}
final UserHistoryDictionary dict =
- new UserHistoryDictionary(context, locale, dictTypeId, sp);
+ new UserHistoryDictionary(context, locale, sp);
sLangDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict));
return dict;
}
- private UserHistoryDictionary(final Context context, final String locale, final int dicTypeId,
- SharedPreferences sp) {
- super(context, dicTypeId);
+ private UserHistoryDictionary(final Context context, final String locale,
+ final SharedPreferences sp) {
+ super(context, Dictionary.TYPE_USER_HISTORY);
mLocale = locale;
mPrefs = sp;
if (sOpenHelper == null) {
@@ -158,6 +156,14 @@ public class UserHistoryDictionary extends ExpandableDictionary {
// super.close();
}
+ @Override
+ protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ // Inhibit suggestions (not predictions) for user history for now. Removing this method
+ // is enough to use it through the standard ExpandableDictionary way.
+ return null;
+ }
+
/**
* Return whether the passed charsequence is in the dictionary.
*/
@@ -492,9 +498,11 @@ public class UserHistoryDictionary extends ExpandableDictionary {
needsToSave(fc, isValid, addLevel0Bigram)) {
freq = fc;
} else {
+ // Delete this entry
freq = -1;
}
} else {
+ // Delete this entry
freq = -1;
}
}
@@ -531,6 +539,7 @@ public class UserHistoryDictionary extends ExpandableDictionary {
getContentValues(word1, word2, mLocale));
pairId = pairIdLong.intValue();
}
+ // Eliminate freq == 0 because that word is profanity.
if (freq > 0) {
if (PROFILE_SAVE_RESTORE) {
++profInsert;
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
index 28847745e..bb0f5429a 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
@@ -29,9 +29,8 @@ import java.util.Set;
public class UserHistoryDictionaryBigramList {
public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0;
private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName();
- private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = new HashMap<String, Byte>();
- private final HashMap<String, HashMap<String, Byte>> mBigramMap =
- new HashMap<String, HashMap<String, Byte>>();
+ private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = CollectionUtils.newHashMap();
+ private final HashMap<String, HashMap<String, Byte>> mBigramMap = CollectionUtils.newHashMap();
private int mSize = 0;
public void evictAll() {
@@ -57,7 +56,7 @@ public class UserHistoryDictionaryBigramList {
if (mBigramMap.containsKey(word1)) {
map = mBigramMap.get(word1);
} else {
- map = new HashMap<String, Byte>();
+ map = CollectionUtils.newHashMap();
mBigramMap.put(word1, map);
}
if (!map.containsKey(word2)) {
@@ -98,11 +97,11 @@ public class UserHistoryDictionaryBigramList {
}
public HashMap<String, Byte> getBigrams(String word1) {
- if (!mBigramMap.containsKey(word1)) {
- return EMPTY_BIGRAM_MAP;
- } else {
- return mBigramMap.get(word1);
- }
+ if (mBigramMap.containsKey(word1)) return mBigramMap.get(word1);
+ // TODO: lower case according to locale
+ final String lowerWord1 = word1.toLowerCase();
+ if (mBigramMap.containsKey(lowerWord1)) return mBigramMap.get(lowerWord1);
+ return EMPTY_BIGRAM_MAP;
}
public boolean removeBigram(String word1, String word2) {
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
index e5516dc62..5a2fdf48e 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
@@ -50,7 +50,7 @@ public class UserHistoryForgettingCurveUtils {
}
private ForgettingCurveParams(long now, boolean isValid) {
- this((int)pushCount((byte)0, isValid), now, now, isValid);
+ this(pushCount((byte)0, isValid), now, now, isValid);
}
/** This constructor is called when the user history bigram dictionary is being restored. */
@@ -199,20 +199,20 @@ public class UserHistoryForgettingCurveUtils {
public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
static {
for (int i = 0; i < FC_LEVEL_MAX; ++i) {
- final double initialFreq;
+ final float initialFreq;
if (i >= 2) {
- initialFreq = (double)FC_FREQ_MAX;
+ initialFreq = FC_FREQ_MAX;
} else if (i == 1) {
- initialFreq = (double)FC_FREQ_MAX / 2;
+ initialFreq = FC_FREQ_MAX / 2;
} else if (i == 0) {
- initialFreq = (double)FC_FREQ_MAX / 4;
+ initialFreq = FC_FREQ_MAX / 4;
} else {
continue;
}
for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
- final double elapsedHour = j * ELAPSED_TIME_INTERVAL_HOURS;
- final double freq =
- initialFreq * Math.pow(initialFreq, elapsedHour / HALF_LIFE_HOURS);
+ final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
+ final float freq = initialFreq
+ * (float)Math.pow(initialFreq, elapsedHours / HALF_LIFE_HOURS);
final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq));
SCORE_TABLE[i][j] = intFreq;
}
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index 4178955bc..fc7a42100 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -44,10 +44,8 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
-import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
-import java.util.Map;
public class Utils {
private Utils() {
@@ -67,44 +65,6 @@ public class Utils {
}
}
- public static class GCUtils {
- private static final String GC_TAG = GCUtils.class.getSimpleName();
- public static final int GC_TRY_COUNT = 2;
- // GC_TRY_LOOP_MAX is used for the hard limit of GC wait,
- // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT.
- public static final int GC_TRY_LOOP_MAX = 5;
- private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS;
- private static GCUtils sInstance = new GCUtils();
- private int mGCTryCount = 0;
-
- public static GCUtils getInstance() {
- return sInstance;
- }
-
- public void reset() {
- mGCTryCount = 0;
- }
-
- public boolean tryGCOrWait(String metaData, Throwable t) {
- if (mGCTryCount == 0) {
- System.gc();
- }
- if (++mGCTryCount > GC_TRY_COUNT) {
- LatinImeLogger.logOnException(metaData, t);
- return false;
- } else {
- try {
- Thread.sleep(GC_INTERVAL);
- return true;
- } catch (InterruptedException e) {
- Log.e(GC_TAG, "Sleep was interrupted.");
- LatinImeLogger.logOnException(metaData, t);
- return false;
- }
- }
- }
- }
-
/* package */ static class RingCharBuffer {
private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
@@ -206,18 +166,24 @@ public class Utils {
}
// Get the current stack trace
- public static String getStackTrace() {
+ public static String getStackTrace(final int limit) {
StringBuilder sb = new StringBuilder();
try {
throw new RuntimeException();
} catch (RuntimeException e) {
StackTraceElement[] frames = e.getStackTrace();
// Start at 1 because the first frame is here and we don't care about it
- for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n");
+ for (int j = 1; j < frames.length && j < limit + 1; ++j) {
+ sb.append(frames[j].toString() + "\n");
+ }
}
return sb.toString();
}
+ public static String getStackTrace() {
+ return getStackTrace(Integer.MAX_VALUE - 1);
+ }
+
public static class UsabilityStudyLogUtils {
// TODO: remove code duplication with ResearchLog class
private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
@@ -473,7 +439,7 @@ public class Utils {
private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
private static final HashMap<String, String> sDeviceOverrideValueMap =
- new HashMap<String, String>();
+ CollectionUtils.newHashMap();
public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
final int orientation = res.getConfiguration().orientation;
@@ -491,7 +457,7 @@ public class Utils {
return sDeviceOverrideValueMap.get(key);
}
- private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = new HashMap<String, Long>();
+ private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap();
private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
if (TextUtils.isEmpty(str)) {
@@ -502,7 +468,7 @@ public class Utils {
if (N < 2 || N % 2 != 0) {
return EMPTY_LT_HASH_MAP;
}
- final HashMap<String, Long> retval = new HashMap<String, Long>();
+ final HashMap<String, Long> retval = CollectionUtils.newHashMap();
for (int i = 0; i < N / 2; ++i) {
final String localeStr = ss[i * 2];
final long time = Long.valueOf(ss[i * 2 + 1]);
diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
deleted file mode 100644
index a0de2f970..000000000
--- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * 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.android.inputmethod.latin;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
-
-import java.util.HashMap;
-import java.util.Locale;
-
-public class WhitelistDictionary extends ExpandableDictionary {
-
- private static final boolean DBG = LatinImeLogger.sDBG;
- private static final String TAG = WhitelistDictionary.class.getSimpleName();
-
- private final HashMap<String, Pair<Integer, String>> mWhitelistWords =
- new HashMap<String, Pair<Integer, String>>();
-
- // TODO: Conform to the async load contact of ExpandableDictionary
- public WhitelistDictionary(final Context context, final Locale locale) {
- super(context, Suggest.DIC_WHITELIST);
- // TODO: Move whitelist dictionary into main dictionary.
- final RunInLocale<Void> job = new RunInLocale<Void>() {
- @Override
- protected Void job(Resources res) {
- initWordlist(res.getStringArray(R.array.wordlist_whitelist));
- return null;
- }
- };
- job.runInLocale(context.getResources(), locale);
- }
-
- private void initWordlist(String[] wordlist) {
- mWhitelistWords.clear();
- final int N = wordlist.length;
- if (N % 3 != 0) {
- if (DBG) {
- Log.d(TAG, "The number of the whitelist is invalid.");
- }
- return;
- }
- try {
- for (int i = 0; i < N; i += 3) {
- final int score = Integer.valueOf(wordlist[i]);
- final String before = wordlist[i + 1];
- final String after = wordlist[i + 2];
- if (before != null && after != null) {
- mWhitelistWords.put(
- before.toLowerCase(), new Pair<Integer, String>(score, after));
- addWord(after, null /* shortcut */, score);
- }
- }
- } catch (NumberFormatException e) {
- if (DBG) {
- Log.d(TAG, "The score of the word is invalid.");
- }
- }
- }
-
- public String getWhitelistedWord(String before) {
- if (before == null) return null;
- final String lowerCaseBefore = before.toLowerCase();
- if(mWhitelistWords.containsKey(lowerCaseBefore)) {
- if (DBG) {
- Log.d(TAG, "--- found whitelistedWord: " + lowerCaseBefore);
- }
- return mWhitelistWords.get(lowerCaseBefore).second;
- }
- return null;
- }
-
- // See LatinIME#updateSuggestions. This breaks in the (queer) case that the whitelist
- // lists that word a should autocorrect to word b, and word c would autocorrect to
- // an upper-cased version of a. In this case, the way this return value is used would
- // remove the first candidate when the user typed the upper-cased version of A.
- // Example : abc -> def and xyz -> Abc
- // A user typing Abc would experience it being autocorrected to something else (not
- // necessarily def).
- // There is no such combination in the whitelist at the time and there probably won't
- // ever be - it doesn't make sense. But still.
- public boolean shouldForciblyAutoCorrectFrom(CharSequence word) {
- if (TextUtils.isEmpty(word)) return false;
- final String correction = getWhitelistedWord(word.toString());
- if (TextUtils.isEmpty(correction)) return false;
- return !correction.equals(word);
- }
-
- // Leave implementation of getWords and isValidWord to the superclass.
- // The words have been added to the ExpandableDictionary with addWord() inside initWordlist.
-}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index ca9caa1d3..ecec60f89 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -17,9 +17,7 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardActionListener;
import java.util.Arrays;
@@ -27,22 +25,27 @@ import java.util.Arrays;
* A place to store the currently composing word with information such as adjacent key codes as well
*/
public class WordComposer {
-
- public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
- public static final int NOT_A_COORDINATE = -1;
-
private static final int N = BinaryDictionary.MAX_WORD_LENGTH;
+ public static final int CAPS_MODE_OFF = 0;
+ // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
+ // aren't used anywhere in the code
+ public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
+ public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
+ public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
+ public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
+
private int[] mPrimaryKeyCodes;
- private int[] mXCoordinates;
- private int[] mYCoordinates;
- private StringBuilder mTypedWord;
+ private final InputPointers mInputPointers = new InputPointers(N);
+ private final StringBuilder mTypedWord;
private CharSequence mAutoCorrection;
private boolean mIsResumed;
+ private boolean mIsBatchMode;
// Cache these values for performance
private int mCapsCount;
- private boolean mAutoCapitalized;
+ private int mDigitsCount;
+ private int mCapitalizedMode;
private int mTrailingSingleQuotesCount;
private int mCodePointSize;
@@ -54,28 +57,24 @@ public class WordComposer {
public WordComposer() {
mPrimaryKeyCodes = new int[N];
mTypedWord = new StringBuilder(N);
- mXCoordinates = new int[N];
- mYCoordinates = new int[N];
mAutoCorrection = null;
mTrailingSingleQuotesCount = 0;
mIsResumed = false;
+ mIsBatchMode = false;
refreshSize();
}
public WordComposer(WordComposer source) {
- init(source);
- }
-
- public void init(WordComposer source) {
mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
mTypedWord = new StringBuilder(source.mTypedWord);
- mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length);
- mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length);
+ mInputPointers.copy(source.mInputPointers);
mCapsCount = source.mCapsCount;
+ mDigitsCount = source.mDigitsCount;
mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
- mAutoCapitalized = source.mAutoCapitalized;
+ mCapitalizedMode = source.mCapitalizedMode;
mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
mIsResumed = source.mIsResumed;
+ mIsBatchMode = source.mIsBatchMode;
refreshSize();
}
@@ -86,13 +85,15 @@ public class WordComposer {
mTypedWord.setLength(0);
mAutoCorrection = null;
mCapsCount = 0;
+ mDigitsCount = 0;
mIsFirstCharCapitalized = false;
mTrailingSingleQuotesCount = 0;
mIsResumed = false;
+ mIsBatchMode = false;
refreshSize();
}
- public final void refreshSize() {
+ private final void refreshSize() {
mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
}
@@ -116,12 +117,8 @@ public class WordComposer {
return mPrimaryKeyCodes[index];
}
- public int[] getXCoordinates() {
- return mXCoordinates;
- }
-
- public int[] getYCoordinates() {
- return mYCoordinates;
+ public InputPointers getInputPointers() {
+ return mInputPointers;
}
private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) {
@@ -129,40 +126,28 @@ public class WordComposer {
return previous && !Character.isUpperCase(codePoint);
}
- // TODO: remove input keyDetector
- public void add(int primaryCode, int x, int y, KeyDetector keyDetector) {
- final int keyX;
- final int keyY;
- if (null == keyDetector
- || x == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
- || y == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
- || x == KeyboardActionListener.NOT_A_TOUCH_COORDINATE
- || y == KeyboardActionListener.NOT_A_TOUCH_COORDINATE) {
- keyX = x;
- keyY = y;
- } else {
- keyX = keyDetector.getTouchX(x);
- keyY = keyDetector.getTouchY(y);
- }
- add(primaryCode, keyX, keyY);
- }
-
/**
* Add a new keystroke, with the pressed key's code point with the touch point coordinates.
*/
- private void add(int primaryCode, int keyX, int keyY) {
+ public void add(int primaryCode, int keyX, int keyY) {
final int newIndex = size();
mTypedWord.appendCodePoint(primaryCode);
refreshSize();
if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
? Character.toLowerCase(primaryCode) : primaryCode;
- mXCoordinates[newIndex] = keyX;
- mYCoordinates[newIndex] = keyY;
+ // In the batch input mode, the {@code mInputPointers} holds batch input points and
+ // shouldn't be overridden by the "typed key" coordinates
+ // (See {@link #setBatchInputWord}).
+ if (!mIsBatchMode) {
+ // TODO: Set correct pointer id and time
+ mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
+ }
}
mIsFirstCharCapitalized = isFirstCharCapitalized(
newIndex, primaryCode, mIsFirstCharCapitalized);
if (Character.isUpperCase(primaryCode)) mCapsCount++;
+ if (Character.isDigit(primaryCode)) mDigitsCount++;
if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
++mTrailingSingleQuotesCount;
} else {
@@ -171,19 +156,35 @@ public class WordComposer {
mAutoCorrection = null;
}
+ public void setBatchInputPointers(InputPointers batchPointers) {
+ mInputPointers.set(batchPointers);
+ mIsBatchMode = true;
+ }
+
+ public void setBatchInputWord(CharSequence word) {
+ reset();
+ mIsBatchMode = true;
+ final int length = word.length();
+ for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
+ final int codePoint = Character.codePointAt(word, i);
+ // We don't want to override the batch input points that are held in mInputPointers
+ // (See {@link #add(int,int,int)}).
+ add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ }
+ }
+
/**
* Internal method to retrieve reasonable proximity info for a character.
*/
private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
- for (final Key key : keyboard.mKeys) {
- if (key.mCode == codePoint) {
- final int x = key.mX + key.mWidth / 2;
- final int y = key.mY + key.mHeight / 2;
- add(codePoint, x, y);
- return;
- }
+ final Key key = keyboard.getKey(codePoint);
+ if (key != null) {
+ final int x = key.mX + key.mWidth / 2;
+ final int y = key.mY + key.mHeight / 2;
+ add(codePoint, x, y);
+ return;
}
- add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
+ add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
}
/**
@@ -219,6 +220,7 @@ public class WordComposer {
mTypedWord.deleteCharAt(stringBuilderLength - 1);
}
if (Character.isUpperCase(lastChar)) mCapsCount--;
+ if (Character.isDigit(lastChar)) mDigitsCount--;
refreshSize();
}
// We may have deleted the last one.
@@ -263,7 +265,14 @@ public class WordComposer {
* @return true if all user typed chars are upper case, false otherwise
*/
public boolean isAllUpperCase() {
- return (mCapsCount > 0) && (mCapsCount == size());
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+ || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED
+ || (mCapsCount > 0) && (mCapsCount == size());
+ }
+
+ public boolean wasShiftedNoLock() {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
+ || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
}
/**
@@ -274,20 +283,34 @@ public class WordComposer {
}
/**
- * Saves the reason why the word is capitalized - whether it was automatic or
- * due to the user hitting shift in the middle of a sentence.
- * @param auto whether it was an automatic capitalization due to start of sentence
+ * Returns true if we have digits in the composing word.
+ */
+ public boolean hasDigits() {
+ return mDigitsCount > 0;
+ }
+
+ /**
+ * Saves the caps mode at the start of composing.
+ *
+ * WordComposer needs to know about this for several reasons. The first is, we need to know
+ * after the fact what the reason was, to register the correct form into the user history
+ * dictionary: if the word was automatically capitalized, we should insert it in all-lower
+ * case but if it's a manual pressing of shift, then it should be inserted as is.
+ * Also, batch input needs to know about the current caps mode to display correctly
+ * capitalized suggestions.
+ * @param mode the mode at the time of start
*/
- public void setAutoCapitalized(boolean auto) {
- mAutoCapitalized = auto;
+ public void setCapitalizedModeAtStartComposingTime(final int mode) {
+ mCapitalizedMode = mode;
}
/**
* Returns whether the word was automatically capitalized.
* @return whether the word was automatically capitalized
*/
- public boolean isAutoCapitalized() {
- return mAutoCapitalized;
+ public boolean wasAutoCapitalized() {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+ || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
}
/**
@@ -318,19 +341,21 @@ public class WordComposer {
// or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
// the last composed word to ensure this does not happen.
final int[] primaryKeyCodes = mPrimaryKeyCodes;
- final int[] xCoordinates = mXCoordinates;
- final int[] yCoordinates = mYCoordinates;
mPrimaryKeyCodes = new int[N];
- mXCoordinates = new int[N];
- mYCoordinates = new int[N];
final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
- xCoordinates, yCoordinates, mTypedWord.toString(), committedWord, separatorCode,
+ mInputPointers, mTypedWord.toString(), committedWord, separatorCode,
prevWord);
+ mInputPointers.reset();
if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
&& type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
lastComposedWord.deactivate();
}
+ mCapsCount = 0;
+ mDigitsCount = 0;
+ mIsBatchMode = false;
mTypedWord.setLength(0);
+ mTrailingSingleQuotesCount = 0;
+ mIsFirstCharCapitalized = false;
refreshSize();
mAutoCorrection = null;
mIsResumed = false;
@@ -339,12 +364,15 @@ public class WordComposer {
public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
- mXCoordinates = lastComposedWord.mXCoordinates;
- mYCoordinates = lastComposedWord.mYCoordinates;
+ mInputPointers.set(lastComposedWord.mInputPointers);
mTypedWord.setLength(0);
mTypedWord.append(lastComposedWord.mTypedWord);
refreshSize();
mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
mIsResumed = true;
}
+
+ public boolean isBatchMode() {
+ return mIsBatchMode;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
index 89c59f809..161b94ca0 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -22,10 +22,13 @@ import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
-import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -124,7 +127,7 @@ public class BinaryDictInputOutput {
*/
private static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
- private static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
+ public static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
private static final int MINIMUM_SUPPORTED_VERSION = 1;
private static final int MAXIMUM_SUPPORTED_VERSION = 2;
private static final int NOT_A_VERSION_NUMBER = -1;
@@ -307,33 +310,32 @@ public class BinaryDictInputOutput {
}
/**
- * Reads a string from a RandomAccessFile. This is the converse of the above method.
+ * Reads a string from a ByteBuffer. This is the converse of the above method.
*/
- private static String readString(final RandomAccessFile source) throws IOException {
+ private static String readString(final ByteBuffer buffer) {
final StringBuilder s = new StringBuilder();
- int character = readChar(source);
+ int character = readChar(buffer);
while (character != INVALID_CHARACTER) {
s.appendCodePoint(character);
- character = readChar(source);
+ character = readChar(buffer);
}
return s.toString();
}
/**
- * Reads a character from the file.
+ * Reads a character from the ByteBuffer.
*
* This follows the character format documented earlier in this source file.
*
- * @param source the file, positioned over an encoded character.
+ * @param buffer the buffer, positioned over an encoded character.
* @return the character code.
*/
- private static int readChar(RandomAccessFile source) throws IOException {
- int character = source.readUnsignedByte();
+ private static int readChar(final ByteBuffer buffer) {
+ int character = readUnsignedByte(buffer);
if (!fitsOnOneByte(character)) {
- if (GROUP_CHARACTERS_TERMINATOR == character)
- return INVALID_CHARACTER;
+ if (GROUP_CHARACTERS_TERMINATOR == character) return INVALID_CHARACTER;
character <<= 16;
- character += source.readUnsignedShort();
+ character += readUnsignedShort(buffer);
}
return character;
}
@@ -783,13 +785,13 @@ public class BinaryDictInputOutput {
// their lower bound and exclude their higher bound so we need to have the first step
// start at exactly 1 unit higher than floor(unigramFreq + half a step).
// Note : to reconstruct the score, the dictionary reader will need to divide
- // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise, and add
- // (discretizedFrequency + 0.5) times this value to get the median value of the step,
- // which is the best approximation. This is how we get the most precise result with
- // only four bits.
- final double stepSize =
- (double)(MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5 + MAX_BIGRAM_FREQUENCY);
- final double firstStepStart = 1 + unigramFrequency + (stepSize / 2.0);
+ // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step,
+ // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best
+ // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the
+ // step pointed by the discretized frequency.
+ final float stepSize =
+ (MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5f + MAX_BIGRAM_FREQUENCY);
+ final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f);
final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize);
// If the bigram freq is less than half-a-step higher than the unigram freq, we get -1
// here. The best approximation would be the unigram freq itself, so we should not
@@ -1091,46 +1093,46 @@ public class BinaryDictInputOutput {
// readDictionaryBinary is the public entry point for them.
static final int[] characterBuffer = new int[MAX_WORD_LENGTH];
- private static CharGroupInfo readCharGroup(RandomAccessFile source,
- final int originalGroupAddress) throws IOException {
+ private static CharGroupInfo readCharGroup(final ByteBuffer buffer,
+ final int originalGroupAddress) {
int addressPointer = originalGroupAddress;
- final int flags = source.readUnsignedByte();
+ final int flags = readUnsignedByte(buffer);
++addressPointer;
final int characters[];
if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
int index = 0;
- int character = CharEncoding.readChar(source);
+ int character = CharEncoding.readChar(buffer);
addressPointer += CharEncoding.getCharSize(character);
while (-1 != character) {
characterBuffer[index++] = character;
- character = CharEncoding.readChar(source);
+ character = CharEncoding.readChar(buffer);
addressPointer += CharEncoding.getCharSize(character);
}
characters = Arrays.copyOfRange(characterBuffer, 0, index);
} else {
- final int character = CharEncoding.readChar(source);
+ final int character = CharEncoding.readChar(buffer);
addressPointer += CharEncoding.getCharSize(character);
characters = new int[] { character };
}
final int frequency;
if (0 != (FLAG_IS_TERMINAL & flags)) {
++addressPointer;
- frequency = source.readUnsignedByte();
+ frequency = readUnsignedByte(buffer);
} else {
frequency = CharGroup.NOT_A_TERMINAL;
}
int childrenAddress = addressPointer;
switch (flags & MASK_GROUP_ADDRESS_TYPE) {
case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
- childrenAddress += source.readUnsignedByte();
+ childrenAddress += readUnsignedByte(buffer);
addressPointer += 1;
break;
case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
- childrenAddress += source.readUnsignedShort();
+ childrenAddress += readUnsignedShort(buffer);
addressPointer += 2;
break;
case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
- childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
+ childrenAddress += readUnsignedInt24(buffer);
addressPointer += 3;
break;
case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
@@ -1140,38 +1142,38 @@ public class BinaryDictInputOutput {
}
ArrayList<WeightedString> shortcutTargets = null;
if (0 != (flags & FLAG_HAS_SHORTCUT_TARGETS)) {
- final long pointerBefore = source.getFilePointer();
+ final int pointerBefore = buffer.position();
shortcutTargets = new ArrayList<WeightedString>();
- source.readUnsignedShort(); // Skip the size
+ buffer.getShort(); // Skip the size
while (true) {
- final int targetFlags = source.readUnsignedByte();
- final String word = CharEncoding.readString(source);
+ final int targetFlags = readUnsignedByte(buffer);
+ final String word = CharEncoding.readString(buffer);
shortcutTargets.add(new WeightedString(word,
targetFlags & FLAG_ATTRIBUTE_FREQUENCY));
if (0 == (targetFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
}
- addressPointer += (source.getFilePointer() - pointerBefore);
+ addressPointer += buffer.position() - pointerBefore;
}
ArrayList<PendingAttribute> bigrams = null;
if (0 != (flags & FLAG_HAS_BIGRAMS)) {
bigrams = new ArrayList<PendingAttribute>();
while (true) {
- final int bigramFlags = source.readUnsignedByte();
+ final int bigramFlags = readUnsignedByte(buffer);
++addressPointer;
final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
int bigramAddress = addressPointer;
switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
- bigramAddress += sign * source.readUnsignedByte();
+ bigramAddress += sign * readUnsignedByte(buffer);
addressPointer += 1;
break;
case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
- bigramAddress += sign * source.readUnsignedShort();
+ bigramAddress += sign * readUnsignedShort(buffer);
addressPointer += 2;
break;
case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
- final int offset = ((source.readUnsignedByte() << 16)
- + source.readUnsignedShort());
+ final int offset = (readUnsignedByte(buffer) << 16)
+ + readUnsignedShort(buffer);
bigramAddress += sign * offset;
addressPointer += 3;
break;
@@ -1188,15 +1190,15 @@ public class BinaryDictInputOutput {
}
/**
- * Reads and returns the char group count out of a file and forwards the pointer.
+ * Reads and returns the char group count out of a buffer and forwards the pointer.
*/
- private static int readCharGroupCount(RandomAccessFile source) throws IOException {
- final int msb = source.readUnsignedByte();
+ private static int readCharGroupCount(final ByteBuffer buffer) {
+ final int msb = readUnsignedByte(buffer);
if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
return msb;
} else {
return ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
- + source.readUnsignedByte();
+ + readUnsignedByte(buffer);
}
}
@@ -1204,31 +1206,29 @@ public class BinaryDictInputOutput {
// of this method. Since it performs direct, unbuffered random access to the file and
// may be called hundreds of thousands of times, the resulting performance is not
// reasonable without some kind of cache. Thus:
- // TODO: perform buffered I/O here and in other places in the code.
private static TreeMap<Integer, String> wordCache = new TreeMap<Integer, String>();
/**
* Finds, as a string, the word at the address passed as an argument.
*
- * @param source the file to read from.
+ * @param buffer the buffer to read from.
* @param headerSize the size of the header.
* @param address the address to seek.
* @return the word, as a string.
- * @throws IOException if the file can't be read.
*/
- private static String getWordAtAddress(final RandomAccessFile source, final long headerSize,
- int address) throws IOException {
+ private static String getWordAtAddress(final ByteBuffer buffer, final int headerSize,
+ final int address) {
final String cachedString = wordCache.get(address);
if (null != cachedString) return cachedString;
- final long originalPointer = source.getFilePointer();
- source.seek(headerSize);
- final int count = readCharGroupCount(source);
+ final int originalPointer = buffer.position();
+ buffer.position(headerSize);
+ final int count = readCharGroupCount(buffer);
int groupOffset = getGroupCountSize(count);
final StringBuilder builder = new StringBuilder();
String result = null;
CharGroupInfo last = null;
for (int i = count - 1; i >= 0; --i) {
- CharGroupInfo info = readCharGroup(source, groupOffset);
+ CharGroupInfo info = readCharGroup(buffer, groupOffset);
groupOffset = info.mEndAddress;
if (info.mOriginalAddress == address) {
builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
@@ -1239,9 +1239,9 @@ public class BinaryDictInputOutput {
if (info.mChildrenAddress > address) {
if (null == last) continue;
builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- source.seek(last.mChildrenAddress + headerSize);
+ buffer.position(last.mChildrenAddress + headerSize);
groupOffset = last.mChildrenAddress + 1;
- i = source.readUnsignedByte();
+ i = readUnsignedByte(buffer);
last = null;
continue;
}
@@ -1249,14 +1249,14 @@ public class BinaryDictInputOutput {
}
if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- source.seek(last.mChildrenAddress + headerSize);
+ buffer.position(last.mChildrenAddress + headerSize);
groupOffset = last.mChildrenAddress + 1;
- i = source.readUnsignedByte();
+ i = readUnsignedByte(buffer);
last = null;
continue;
}
}
- source.seek(originalPointer);
+ buffer.position(originalPointer);
wordCache.put(address, result);
return result;
}
@@ -1269,44 +1269,47 @@ public class BinaryDictInputOutput {
* This will recursively read other nodes into the structure, populating the reverse
* maps on the fly and using them to keep track of already read nodes.
*
- * @param source the data file, correctly positioned at the start of a node.
+ * @param buffer the buffer, correctly positioned at the start of a node.
* @param headerSize the size, in bytes, of the file header.
* @param reverseNodeMap a mapping from addresses to already read nodes.
* @param reverseGroupMap a mapping from addresses to already read character groups.
* @return the read node with all his children already read.
*/
- private static Node readNode(RandomAccessFile source, long headerSize,
- Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap)
+ private static Node readNode(final ByteBuffer buffer, final int headerSize,
+ final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap)
throws IOException {
- final int nodeOrigin = (int)(source.getFilePointer() - headerSize);
- final int count = readCharGroupCount(source);
+ final int nodeOrigin = buffer.position() - headerSize;
+ final int count = readCharGroupCount(buffer);
final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
int groupOffset = nodeOrigin + getGroupCountSize(count);
for (int i = count; i > 0; --i) {
- CharGroupInfo info = readCharGroup(source, groupOffset);
+ CharGroupInfo info =readCharGroup(buffer, groupOffset);
ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
ArrayList<WeightedString> bigrams = null;
if (null != info.mBigrams) {
bigrams = new ArrayList<WeightedString>();
for (PendingAttribute bigram : info.mBigrams) {
- final String word = getWordAtAddress(source, headerSize, bigram.mAddress);
+ final String word = getWordAtAddress(
+ buffer, headerSize, bigram.mAddress);
bigrams.add(new WeightedString(word, bigram.mFrequency));
}
}
if (hasChildrenAddress(info.mChildrenAddress)) {
Node children = reverseNodeMap.get(info.mChildrenAddress);
if (null == children) {
- final long currentPosition = source.getFilePointer();
- source.seek(info.mChildrenAddress + headerSize);
- children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap);
- source.seek(currentPosition);
+ final int currentPosition = buffer.position();
+ buffer.position(info.mChildrenAddress + headerSize);
+ children = readNode(
+ buffer, headerSize, reverseNodeMap, reverseGroupMap);
+ buffer.position(currentPosition);
}
nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency,
- children));
+ new CharGroup(info.mCharacters, shortcutTargets,
+ bigrams, info.mFrequency, children));
} else {
nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency));
+ new CharGroup(info.mCharacters, shortcutTargets,
+ bigrams, info.mFrequency));
}
groupOffset = info.mEndAddress;
}
@@ -1318,57 +1321,76 @@ public class BinaryDictInputOutput {
/**
* Helper function to get the binary format version from the header.
+ * @throws IOException
*/
- private static int getFormatVersion(final RandomAccessFile source) throws IOException {
- final int magic_v1 = source.readUnsignedShort();
- if (VERSION_1_MAGIC_NUMBER == magic_v1) return source.readUnsignedByte();
- final int magic_v2 = (magic_v1 << 16) + source.readUnsignedShort();
- if (VERSION_2_MAGIC_NUMBER == magic_v2) return source.readUnsignedShort();
+ private static int getFormatVersion(final ByteBuffer buffer) throws IOException {
+ final int magic_v1 = readUnsignedShort(buffer);
+ if (VERSION_1_MAGIC_NUMBER == magic_v1) return readUnsignedByte(buffer);
+ final int magic_v2 = (magic_v1 << 16) + readUnsignedShort(buffer);
+ if (VERSION_2_MAGIC_NUMBER == magic_v2) return readUnsignedShort(buffer);
return NOT_A_VERSION_NUMBER;
}
/**
- * Reads a random access file and returns the memory representation of the dictionary.
+ * Reads options from a file and populate a map with their contents.
+ *
+ * The file is read at the current file pointer, so the caller must take care the pointer
+ * is in the right place before calling this.
+ */
+ public static void populateOptions(final ByteBuffer buffer, final int headerSize,
+ final HashMap<String, String> options) {
+ while (buffer.position() < headerSize) {
+ final String key = CharEncoding.readString(buffer);
+ final String value = CharEncoding.readString(buffer);
+ options.put(key, value);
+ }
+ }
+
+ /**
+ * Reads a byte buffer and returns the memory representation of the dictionary.
*
* This high-level method takes a binary file and reads its contents, populating a
* FusionDictionary structure. The optional dict argument is an existing dictionary to
* which words from the file should be added. If it is null, a new dictionary is created.
*
- * @param source the file to read.
+ * @param buffer the buffer to read.
* @param dict an optional dictionary to add words to, or null.
* @return the created (or merged) dictionary.
*/
- public static FusionDictionary readDictionaryBinary(final RandomAccessFile source,
+ public static FusionDictionary readDictionaryBinary(final ByteBuffer buffer,
final FusionDictionary dict) throws IOException, UnsupportedFormatException {
// Check file version
- final int version = getFormatVersion(source);
- if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION ) {
+ final int version = getFormatVersion(buffer);
+ if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) {
throw new UnsupportedFormatException("This file has version " + version
+ ", but this implementation does not support versions above "
+ MAXIMUM_SUPPORTED_VERSION);
}
+ // clear cache
+ wordCache.clear();
+
// Read options
- final int optionsFlags = source.readUnsignedShort();
+ final int optionsFlags = readUnsignedShort(buffer);
- final long headerSize;
+ final int headerSize;
final HashMap<String, String> options = new HashMap<String, String>();
if (version < FIRST_VERSION_WITH_HEADER_SIZE) {
- headerSize = source.getFilePointer();
+ headerSize = buffer.position();
} else {
- headerSize = (source.readUnsignedByte() << 24) + (source.readUnsignedByte() << 16)
- + (source.readUnsignedByte() << 8) + source.readUnsignedByte();
- while (source.getFilePointer() < headerSize) {
- final String key = CharEncoding.readString(source);
- final String value = CharEncoding.readString(source);
- options.put(key, value);
- }
- source.seek(headerSize);
+ headerSize = buffer.getInt();
+ populateOptions(buffer, headerSize, options);
+ buffer.position(headerSize);
+ }
+
+ if (headerSize < 0) {
+ throw new UnsupportedFormatException("header size can't be negative.");
}
Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
- final Node root = readNode(source, headerSize, reverseNodeMapping, reverseGroupMapping);
+ final Node root = readNode(
+ buffer, headerSize, reverseNodeMapping, reverseGroupMapping);
FusionDictionary newDict = new FusionDictionary(root,
new FusionDictionary.DictionaryOptions(options,
@@ -1392,6 +1414,28 @@ public class BinaryDictInputOutput {
}
/**
+ * Helper function to read one byte from ByteBuffer.
+ */
+ private static int readUnsignedByte(final ByteBuffer buffer) {
+ return ((int)buffer.get()) & 0xFF;
+ }
+
+ /**
+ * Helper function to read two byte from ByteBuffer.
+ */
+ private static int readUnsignedShort(final ByteBuffer buffer) {
+ return ((int)buffer.getShort()) & 0xFFFF;
+ }
+
+ /**
+ * Helper function to read three byte from ByteBuffer.
+ */
+ private static int readUnsignedInt24(final ByteBuffer buffer) {
+ final int value = readUnsignedByte(buffer) << 16;
+ return value + readUnsignedShort(buffer);
+ }
+
+ /**
* Basic test to find out whether the file is a binary dictionary or not.
*
* Concretely this only tests the magic number.
@@ -1400,14 +1444,44 @@ public class BinaryDictInputOutput {
* @return true if it's a binary dictionary, false otherwise
*/
public static boolean isBinaryDictionary(final String filename) {
+ FileInputStream inStream = null;
try {
- RandomAccessFile f = new RandomAccessFile(filename, "r");
- final int version = getFormatVersion(f);
+ final File file = new File(filename);
+ inStream = new FileInputStream(file);
+ final ByteBuffer buffer = inStream.getChannel().map(
+ FileChannel.MapMode.READ_ONLY, 0, file.length());
+ final int version = getFormatVersion(buffer);
return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION);
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) {
return false;
+ } finally {
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
}
}
+
+ /**
+ * Calculate bigram frequency from compressed value
+ *
+ * @see #makeBigramFlags
+ *
+ * @param unigramFrequency
+ * @param bigramFrequency compressed frequency
+ * @return approximate bigram frequency
+ */
+ public static int reconstructBigramFrequency(final int unigramFrequency,
+ final int bigramFrequency) {
+ final float stepSize = (MAX_TERMINAL_FREQUENCY - unigramFrequency)
+ / (1.5f + MAX_BIGRAM_FREQUENCY);
+ final float resultFreqFloat = (float)unigramFrequency
+ + stepSize * (bigramFrequency + 1.0f);
+ return (int)resultFreqFloat;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 8b53c9427..7c15ba54d 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -61,8 +61,8 @@ public class FusionDictionary implements Iterable<Word> {
* This represents an "attribute", that is either a bigram or a shortcut.
*/
public static class WeightedString {
- final String mWord;
- int mFrequency;
+ public final String mWord;
+ public int mFrequency;
public WeightedString(String word, int frequency) {
mWord = word;
mFrequency = frequency;
@@ -516,13 +516,23 @@ public class FusionDictionary implements Iterable<Word> {
int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
currentGroup = node.mData.get(indexOfGroup);
+
+ if (s.length() - index < currentGroup.mChars.length) return null;
+ int newIndex = index;
+ while (newIndex < s.length() && newIndex - index < currentGroup.mChars.length) {
+ if (currentGroup.mChars[newIndex - index] != s.codePointAt(newIndex)) return null;
+ newIndex++;
+ }
+ index = newIndex;
+
if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
- index += currentGroup.mChars.length;
if (index < s.length()) {
node = currentGroup.mChildren;
}
} while (null != node && index < s.length());
+ if (index < s.length()) return null;
+ if (!currentGroup.isTerminal()) return null;
if (DBG && !s.equals(checker.toString())) return null;
return currentGroup;
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java
index d07826757..65fc72c40 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Word.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Word.java
@@ -27,10 +27,10 @@ import java.util.Arrays;
* This is chiefly used to iterate a dictionary.
*/
public class Word implements Comparable<Word> {
- final String mWord;
- final int mFrequency;
- final ArrayList<WeightedString> mShortcutTargets;
- final ArrayList<WeightedString> mBigrams;
+ public final String mWord;
+ public final int mFrequency;
+ public final ArrayList<WeightedString> mShortcutTargets;
+ public final ArrayList<WeightedString> mBigrams;
private int mHashCode = 0;
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 88efc5a85..eef7a51f2 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -20,30 +20,22 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.service.textservice.SpellCheckerService;
-import android.text.TextUtils;
import android.util.Log;
-import android.util.LruCache;
-import android.view.textservice.SentenceSuggestionsInfo;
import android.view.textservice.SuggestionsInfo;
-import android.view.textservice.TextInfo;
-import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.ContactsBinaryDictionary;
import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.Dictionary.WordCallback;
import com.android.inputmethod.latin.DictionaryCollection;
import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LocaleUtils;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.StringUtils;
import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary;
-import com.android.inputmethod.latin.SynchronouslyLoadedContactsDictionary;
import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
-import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
-import com.android.inputmethod.latin.WhitelistDictionary;
-import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.UserBinaryDictionary;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -66,18 +58,15 @@ public class AndroidSpellCheckerService extends SpellCheckerService
public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
- private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
- private static final int CAPITALIZE_FIRST = 1; // First only
- private static final int CAPITALIZE_ALL = 2; // All caps
+ public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
+ public static final int CAPITALIZE_FIRST = 1; // First only
+ public static final int CAPITALIZE_ALL = 2; // All caps
private final static String[] EMPTY_STRING_ARRAY = new String[0];
- private Map<String, DictionaryPool> mDictionaryPools =
- Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
- private Map<String, Dictionary> mUserDictionaries =
- Collections.synchronizedMap(new TreeMap<String, Dictionary>());
- private Map<String, Dictionary> mWhitelistDictionaries =
- Collections.synchronizedMap(new TreeMap<String, Dictionary>());
- private Dictionary mContactsDictionary;
+ private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
+ private Map<String, UserBinaryDictionary> mUserDictionaries =
+ CollectionUtils.newSynchronizedTreeMap();
+ private ContactsBinaryDictionary mContactsDictionary;
// The threshold for a candidate to be offered as a suggestion.
private float mSuggestionThreshold;
@@ -88,12 +77,12 @@ public class AndroidSpellCheckerService extends SpellCheckerService
private final Object mUseContactsLock = new Object();
private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
- new HashSet<WeakReference<DictionaryCollection>>();
+ CollectionUtils.newHashSet();
public static final int SCRIPT_LATIN = 0;
public static final int SCRIPT_CYRILLIC = 1;
- private static final String SINGLE_QUOTE = "\u0027";
- private static final String APOSTROPHE = "\u2019";
+ public static final String SINGLE_QUOTE = "\u0027";
+ public static final String APOSTROPHE = "\u2019";
private static final TreeMap<String, Integer> mLanguageToScript;
static {
// List of the supported languages and their associated script. We won't check
@@ -104,7 +93,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
// proximity to pass to the dictionary descent algorithm.
// IMPORTANT: this only contains languages - do not write countries in there.
// Only the language is searched from the map.
- mLanguageToScript = new TreeMap<String, Integer>();
+ mLanguageToScript = CollectionUtils.newTreeMap();
mLanguageToScript.put("en", SCRIPT_LATIN);
mLanguageToScript.put("fr", SCRIPT_LATIN);
mLanguageToScript.put("de", SCRIPT_LATIN);
@@ -130,7 +119,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
}
- private static int getScriptFromLocale(final Locale locale) {
+ public static int getScriptFromLocale(final Locale locale) {
final Integer script = mLanguageToScript.get(locale.getLanguage());
if (null == script) {
throw new RuntimeException("We have been called with an unsupported language: \""
@@ -154,13 +143,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService
private void startUsingContactsDictionaryLocked() {
if (null == mContactsDictionary) {
- if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
- // TODO: use the right locale for each session
- mContactsDictionary =
- new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
- } else {
- mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
- }
+ // TODO: use the right locale for each session
+ mContactsDictionary =
+ new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
}
final Iterator<WeakReference<DictionaryCollection>> iterator =
mDictionaryCollectionsList.iterator();
@@ -196,19 +181,27 @@ public class AndroidSpellCheckerService extends SpellCheckerService
@Override
public Session createSession() {
- return new AndroidSpellCheckerSession(this);
+ // Should not refer to AndroidSpellCheckerSession directly considering
+ // that AndroidSpellCheckerSession may be overlaid.
+ return AndroidSpellCheckerSessionFactory.newInstance(this);
}
- private static SuggestionsInfo getNotInDictEmptySuggestions() {
+ public static SuggestionsInfo getNotInDictEmptySuggestions() {
return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
}
- private static SuggestionsInfo getInDictEmptySuggestions() {
+ public static SuggestionsInfo getInDictEmptySuggestions() {
return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
EMPTY_STRING_ARRAY);
}
- private static class SuggestionsGatherer implements WordCallback {
+ public SuggestionsGatherer newSuggestionsGatherer(final String text, int maxLength) {
+ return new SuggestionsGatherer(
+ text, mSuggestionThreshold, mRecommendedThreshold, maxLength);
+ }
+
+ // TODO: remove this class and replace it by storage local to the session.
+ public static class SuggestionsGatherer {
public static class Result {
public final String[] mSuggestions;
public final boolean mHasRecommendedSuggestions;
@@ -238,13 +231,12 @@ public class AndroidSpellCheckerService extends SpellCheckerService
mSuggestionThreshold = suggestionThreshold;
mRecommendedThreshold = recommendedThreshold;
mMaxLength = maxLength;
- mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
+ mSuggestions = CollectionUtils.newArrayList(maxLength + 1);
mScores = new int[mMaxLength];
}
- @Override
- synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
- int dicTypeId, int dataType) {
+ synchronized public boolean addWord(char[] word, int[] spaceIndices, int wordOffset,
+ int wordLength, int score) {
final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score);
// binarySearch returns the index if the element exists, and -<insertion index> - 1
// if it doesn't. See documentation for binarySearch.
@@ -367,11 +359,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService
private void closeAllDictionaries() {
final Map<String, DictionaryPool> oldPools = mDictionaryPools;
- mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
- final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
- mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
- final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
- mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+ mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
+ final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries;
+ mUserDictionaries = CollectionUtils.newSynchronizedTreeMap();
new Thread("spellchecker_close_dicts") {
@Override
public void run() {
@@ -381,15 +371,12 @@ public class AndroidSpellCheckerService extends SpellCheckerService
for (Dictionary dict : oldUserDictionaries.values()) {
dict.close();
}
- for (Dictionary dict : oldWhitelistDictionaries.values()) {
- dict.close();
- }
synchronized (mUseContactsLock) {
if (null != mContactsDictionary) {
// The synchronously loaded contacts dictionary should have been in one
// or several pools, but it is shielded against multiple closing and it's
// safe to call it several times.
- final Dictionary dictToClose = mContactsDictionary;
+ final ContactsBinaryDictionary dictToClose = mContactsDictionary;
// TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY
// is no longer needed
mContactsDictionary = null;
@@ -400,7 +387,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
}.start();
}
- private DictionaryPool getDictionaryPool(final String locale) {
+ public DictionaryPool getDictionaryPool(final String locale) {
DictionaryPool pool = mDictionaryPools.get(locale);
if (null == pool) {
final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
@@ -421,36 +408,20 @@ public class AndroidSpellCheckerService extends SpellCheckerService
DictionaryFactory.createMainDictionaryFromManager(this, locale,
true /* useFullEditDistance */);
final String localeStr = locale.toString();
- Dictionary userDictionary = mUserDictionaries.get(localeStr);
+ UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr);
if (null == userDictionary) {
- if (LatinIME.USE_BINARY_USER_DICTIONARY) {
- userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
- } else {
- userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
- }
+ userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
mUserDictionaries.put(localeStr, userDictionary);
}
dictionaryCollection.addDictionary(userDictionary);
- Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
- if (null == whitelistDictionary) {
- whitelistDictionary = new WhitelistDictionary(this, locale);
- mWhitelistDictionaries.put(localeStr, whitelistDictionary);
- }
- dictionaryCollection.addDictionary(whitelistDictionary);
synchronized (mUseContactsLock) {
if (mUseContactsDictionary) {
if (null == mContactsDictionary) {
- // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
- // longer needed
- if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
- // TODO: use the right locale. We can't do it right now because the
- // spell checker is reusing the contacts dictionary across sessions
- // without regard for their locale, so we need to fix that first.
- mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
- Locale.getDefault());
- } else {
- mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
- }
+ // TODO: use the right locale. We can't do it right now because the
+ // spell checker is reusing the contacts dictionary across sessions
+ // without regard for their locale, so we need to fix that first.
+ mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
+ Locale.getDefault());
}
}
dictionaryCollection.addDictionary(mContactsDictionary);
@@ -461,7 +432,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
}
// This method assumes the text is not empty or null.
- private static int getCapitalizationType(String text) {
+ public static int getCapitalizationType(String text) {
// If the first char is not uppercase, then the word is either all lower case,
// and in either case we return CAPITALIZE_NONE.
if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE;
@@ -478,358 +449,4 @@ public class AndroidSpellCheckerService extends SpellCheckerService
if (1 == capsCount) return CAPITALIZE_FIRST;
return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
}
-
- private static class AndroidSpellCheckerSession extends Session {
- // Immutable, but need the locale which is not available in the constructor yet
- private DictionaryPool mDictionaryPool;
- // Likewise
- private Locale mLocale;
- // Cache this for performance
- private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
-
- private final AndroidSpellCheckerService mService;
-
- private final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
-
- private static class SuggestionsParams {
- public final String[] mSuggestions;
- public final int mFlags;
- public SuggestionsParams(String[] suggestions, int flags) {
- mSuggestions = suggestions;
- mFlags = flags;
- }
- }
-
- private static class SuggestionsCache {
- private static final int MAX_CACHE_SIZE = 50;
- // TODO: support bigram
- private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
- new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
-
- public SuggestionsParams getSuggestionsFromCache(String query) {
- return mUnigramSuggestionsInfoCache.get(query);
- }
-
- public void putSuggestionsToCache(String query, String[] suggestions, int flags) {
- if (suggestions == null || TextUtils.isEmpty(query)) {
- return;
- }
- mUnigramSuggestionsInfoCache.put(query, new SuggestionsParams(suggestions, flags));
- }
- }
-
- AndroidSpellCheckerSession(final AndroidSpellCheckerService service) {
- mService = service;
- }
-
- @Override
- public void onCreate() {
- final String localeString = getLocale();
- mDictionaryPool = mService.getDictionaryPool(localeString);
- mLocale = LocaleUtils.constructLocaleFromString(localeString);
- mScript = getScriptFromLocale(mLocale);
- }
-
- /*
- * Returns whether the code point is a letter that makes sense for the specified
- * locale for this spell checker.
- * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
- * and is limited to EFIGS languages and Russian.
- * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
- * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
- */
- private static boolean isLetterCheckableByLanguage(final int codePoint,
- final int script) {
- switch (script) {
- case SCRIPT_LATIN:
- // Our supported latin script dictionaries (EFIGS) at the moment only include
- // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
- // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
- // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
- // excluded from isLetter anyway.
- return codePoint <= 0x2AF && Character.isLetter(codePoint);
- case SCRIPT_CYRILLIC:
- // All Cyrillic characters are in the 400~52F block. There are some in the upper
- // Unicode range, but they are archaic characters that are not used in modern
- // russian and are not used by our dictionary.
- return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
- default:
- // Should never come here
- throw new RuntimeException("Impossible value of script: " + script);
- }
- }
-
- /**
- * Finds out whether a particular string should be filtered out of spell checking.
- *
- * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
- * we know we will never recognize, this accepts a script identifier that should be one
- * of the SCRIPT_* constants defined above, to rule out quickly characters from very
- * different languages.
- *
- * @param text the string to evaluate.
- * @param script the identifier for the script this spell checker recognizes
- * @return true if we should filter this text out, false otherwise
- */
- private static boolean shouldFilterOut(final String text, final int script) {
- if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
-
- // TODO: check if an equivalent processing can't be done more quickly with a
- // compiled regexp.
- // Filter by first letter
- final int firstCodePoint = text.codePointAt(0);
- // Filter out words that don't start with a letter or an apostrophe
- if (!isLetterCheckableByLanguage(firstCodePoint, script)
- && '\'' != firstCodePoint) return true;
-
- // Filter contents
- final int length = text.length();
- int letterCount = 0;
- for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
- final int codePoint = text.codePointAt(i);
- // Any word containing a '@' is probably an e-mail address
- // Any word containing a '/' is probably either an ad-hoc combination of two
- // words or a URI - in either case we don't want to spell check that
- if ('@' == codePoint || '/' == codePoint) return true;
- if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
- }
- // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
- // in this word are letters
- return (letterCount * 4 < length * 3);
- }
-
- private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(
- TextInfo ti, SentenceSuggestionsInfo ssi) {
- final String typedText = ti.getText();
- if (!typedText.contains(SINGLE_QUOTE)) {
- return null;
- }
- final int N = ssi.getSuggestionsCount();
- final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>();
- final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
- final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
- new ArrayList<SuggestionsInfo>();
- for (int i = 0; i < N; ++i) {
- final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
- final int flags = si.getSuggestionsAttributes();
- if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
- continue;
- }
- final int offset = ssi.getOffsetAt(i);
- final int length = ssi.getLengthAt(i);
- final String subText = typedText.substring(offset, offset + length);
- if (!subText.contains(SINGLE_QUOTE)) {
- continue;
- }
- final String[] splitTexts = subText.split(SINGLE_QUOTE, -1);
- if (splitTexts == null || splitTexts.length <= 1) {
- continue;
- }
- final int splitNum = splitTexts.length;
- for (int j = 0; j < splitNum; ++j) {
- final String splitText = splitTexts[j];
- if (TextUtils.isEmpty(splitText)) {
- continue;
- }
- if (mSuggestionsCache.getSuggestionsFromCache(splitText) == null) {
- continue;
- }
- final int newLength = splitText.length();
- // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
- final int newFlags = 0;
- final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
- newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
- if (DBG) {
- Log.d(TAG, "Override and remove old span over: "
- + splitText + ", " + offset + "," + newLength);
- }
- additionalOffsets.add(offset);
- additionalLengths.add(newLength);
- additionalSuggestionsInfos.add(newSi);
- }
- }
- final int additionalSize = additionalOffsets.size();
- if (additionalSize <= 0) {
- return null;
- }
- final int suggestionsSize = N + additionalSize;
- final int[] newOffsets = new int[suggestionsSize];
- final int[] newLengths = new int[suggestionsSize];
- final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
- int i;
- for (i = 0; i < N; ++i) {
- newOffsets[i] = ssi.getOffsetAt(i);
- newLengths[i] = ssi.getLengthAt(i);
- newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
- }
- for (; i < suggestionsSize; ++i) {
- newOffsets[i] = additionalOffsets.get(i - N);
- newLengths[i] = additionalLengths.get(i - N);
- newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
- }
- return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
- }
-
- @Override
- public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(
- TextInfo[] textInfos, int suggestionsLimit) {
- final SentenceSuggestionsInfo[] retval = super.onGetSentenceSuggestionsMultiple(
- textInfos, suggestionsLimit);
- if (retval == null || retval.length != textInfos.length) {
- return retval;
- }
- for (int i = 0; i < retval.length; ++i) {
- final SentenceSuggestionsInfo tempSsi =
- fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
- if (tempSsi != null) {
- retval[i] = tempSsi;
- }
- }
- return retval;
- }
-
- @Override
- public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
- int suggestionsLimit, boolean sequentialWords) {
- final int length = textInfos.length;
- final SuggestionsInfo[] retval = new SuggestionsInfo[length];
- for (int i = 0; i < length; ++i) {
- final String prevWord;
- if (sequentialWords && i > 0) {
- final String prevWordCandidate = textInfos[i - 1].getText();
- // Note that an empty string would be used to indicate the initial word
- // in the future.
- prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
- } else {
- prevWord = null;
- }
- retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
- retval[i].setCookieAndSequence(
- textInfos[i].getCookie(), textInfos[i].getSequence());
- }
- return retval;
- }
-
- // Note : this must be reentrant
- /**
- * Gets a list of suggestions for a specific string. This returns a list of possible
- * corrections for the text passed as an argument. It may split or group words, and
- * even perform grammatical analysis.
- */
- @Override
- public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
- final int suggestionsLimit) {
- return onGetSuggestions(textInfo, null, suggestionsLimit);
- }
-
- private SuggestionsInfo onGetSuggestions(
- final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
- try {
- final String inText = textInfo.getText();
- final SuggestionsParams cachedSuggestionsParams =
- mSuggestionsCache.getSuggestionsFromCache(inText);
- if (cachedSuggestionsParams != null) {
- if (DBG) {
- Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
- }
- return new SuggestionsInfo(
- cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
- }
-
- if (shouldFilterOut(inText, mScript)) {
- DictAndProximity dictInfo = null;
- try {
- dictInfo = mDictionaryPool.takeOrGetNull();
- if (null == dictInfo) return getNotInDictEmptySuggestions();
- return dictInfo.mDictionary.isValidWord(inText) ?
- getInDictEmptySuggestions() : getNotInDictEmptySuggestions();
- } finally {
- if (null != dictInfo) {
- if (!mDictionaryPool.offer(dictInfo)) {
- Log.e(TAG, "Can't re-insert a dictionary into its pool");
- }
- }
- }
- }
- final String text = inText.replaceAll(APOSTROPHE, SINGLE_QUOTE);
-
- // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
- final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
- mService.mSuggestionThreshold, mService.mRecommendedThreshold,
- suggestionsLimit);
- final WordComposer composer = new WordComposer();
- final int length = text.length();
- for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
- final int codePoint = text.codePointAt(i);
- // The getXYForCodePointAndScript method returns (Y << 16) + X
- final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
- codePoint, mScript);
- if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
- composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
- WordComposer.NOT_A_COORDINATE, null);
- } else {
- composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
- }
- }
-
- final int capitalizeType = getCapitalizationType(text);
- boolean isInDict = true;
- DictAndProximity dictInfo = null;
- try {
- dictInfo = mDictionaryPool.takeOrGetNull();
- if (null == dictInfo) return getNotInDictEmptySuggestions();
- dictInfo.mDictionary.getWords(composer, prevWord, suggestionsGatherer,
- dictInfo.mProximityInfo);
- isInDict = dictInfo.mDictionary.isValidWord(text);
- if (!isInDict && CAPITALIZE_NONE != capitalizeType) {
- // We want to test the word again if it's all caps or first caps only.
- // If it's fully down, we already tested it, if it's mixed case, we don't
- // want to test a lowercase version of it.
- isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
- }
- } finally {
- if (null != dictInfo) {
- if (!mDictionaryPool.offer(dictInfo)) {
- Log.e(TAG, "Can't re-insert a dictionary into its pool");
- }
- }
- }
-
- final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
- capitalizeType, mLocale);
-
- if (DBG) {
- Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
- + suggestionsLimit);
- Log.i(TAG, "IsInDict = " + isInDict);
- Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
- Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
- if (null != result.mSuggestions) {
- for (String suggestion : result.mSuggestions) {
- Log.i(TAG, suggestion);
- }
- }
- }
-
- final int flags =
- (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
- : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
- | (result.mHasRecommendedSuggestions
- ? SuggestionsInfoCompatUtils
- .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
- : 0);
- final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
- mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags);
- return retval;
- } catch (RuntimeException e) {
- // Don't kill the keyboard if there is a bug in the spell checker
- if (DBG) {
- throw e;
- } else {
- Log.e(TAG, "Exception while spellcheking: " + e);
- return getNotInDictEmptySuggestions();
- }
- }
- }
- }
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
new file mode 100644
index 000000000..5a1bd37f5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin.spellcheck;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.textservice.SentenceSuggestionsInfo;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.ArrayList;
+
+public class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession {
+ private static final String TAG = AndroidSpellCheckerSession.class.getSimpleName();
+ private static final boolean DBG = false;
+ private final static String[] EMPTY_STRING_ARRAY = new String[0];
+
+ public AndroidSpellCheckerSession(AndroidSpellCheckerService service) {
+ super(service);
+ }
+
+ private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti,
+ SentenceSuggestionsInfo ssi) {
+ final String typedText = ti.getText();
+ if (!typedText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
+ return null;
+ }
+ final int N = ssi.getSuggestionsCount();
+ final ArrayList<Integer> additionalOffsets = CollectionUtils.newArrayList();
+ final ArrayList<Integer> additionalLengths = CollectionUtils.newArrayList();
+ final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
+ CollectionUtils.newArrayList();
+ String currentWord = null;
+ for (int i = 0; i < N; ++i) {
+ final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
+ final int flags = si.getSuggestionsAttributes();
+ if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
+ continue;
+ }
+ final int offset = ssi.getOffsetAt(i);
+ final int length = ssi.getLengthAt(i);
+ final String subText = typedText.substring(offset, offset + length);
+ final String prevWord = currentWord;
+ currentWord = subText;
+ if (!subText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
+ continue;
+ }
+ final String[] splitTexts =
+ subText.split(AndroidSpellCheckerService.SINGLE_QUOTE, -1);
+ if (splitTexts == null || splitTexts.length <= 1) {
+ continue;
+ }
+ final int splitNum = splitTexts.length;
+ for (int j = 0; j < splitNum; ++j) {
+ final String splitText = splitTexts[j];
+ if (TextUtils.isEmpty(splitText)) {
+ continue;
+ }
+ if (mSuggestionsCache.getSuggestionsFromCache(splitText, prevWord) == null) {
+ continue;
+ }
+ final int newLength = splitText.length();
+ // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
+ final int newFlags = 0;
+ final SuggestionsInfo newSi =
+ new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
+ newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
+ if (DBG) {
+ Log.d(TAG, "Override and remove old span over: " + splitText + ", "
+ + offset + "," + newLength);
+ }
+ additionalOffsets.add(offset);
+ additionalLengths.add(newLength);
+ additionalSuggestionsInfos.add(newSi);
+ }
+ }
+ final int additionalSize = additionalOffsets.size();
+ if (additionalSize <= 0) {
+ return null;
+ }
+ final int suggestionsSize = N + additionalSize;
+ final int[] newOffsets = new int[suggestionsSize];
+ final int[] newLengths = new int[suggestionsSize];
+ final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
+ int i;
+ for (i = 0; i < N; ++i) {
+ newOffsets[i] = ssi.getOffsetAt(i);
+ newLengths[i] = ssi.getLengthAt(i);
+ newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
+ }
+ for (; i < suggestionsSize; ++i) {
+ newOffsets[i] = additionalOffsets.get(i - N);
+ newLengths[i] = additionalLengths.get(i - N);
+ newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
+ }
+ return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
+ }
+
+ @Override
+ public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
+ int suggestionsLimit) {
+ final SentenceSuggestionsInfo[] retval =
+ super.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit);
+ if (retval == null || retval.length != textInfos.length) {
+ return retval;
+ }
+ for (int i = 0; i < retval.length; ++i) {
+ final SentenceSuggestionsInfo tempSsi =
+ fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
+ if (tempSsi != null) {
+ retval[i] = tempSsi;
+ }
+ }
+ return retval;
+ }
+
+ @Override
+ public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
+ int suggestionsLimit, boolean sequentialWords) {
+ final int length = textInfos.length;
+ final SuggestionsInfo[] retval = new SuggestionsInfo[length];
+ for (int i = 0; i < length; ++i) {
+ final String prevWord;
+ if (sequentialWords && i > 0) {
+ final String prevWordCandidate = textInfos[i - 1].getText();
+ // Note that an empty string would be used to indicate the initial word
+ // in the future.
+ prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
+ } else {
+ prevWord = null;
+ }
+ retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
+ retval[i].setCookieAndSequence(textInfos[i].getCookie(),
+ textInfos[i].getSequence());
+ }
+ return retval;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java
new file mode 100644
index 000000000..8eb1eb68e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin.spellcheck;
+
+import android.service.textservice.SpellCheckerService.Session;
+
+public abstract class AndroidSpellCheckerSessionFactory {
+ public static Session newInstance(AndroidSpellCheckerService service) {
+ return new AndroidSpellCheckerSession(service);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
new file mode 100644
index 000000000..f4784ff1a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.inputmethod.latin.spellcheck;
+
+import android.service.textservice.SpellCheckerService.Session;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+
+import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LocaleUtils;
+import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+public abstract class AndroidWordLevelSpellCheckerSession extends Session {
+ private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ // Immutable, but need the locale which is not available in the constructor yet
+ private DictionaryPool mDictionaryPool;
+ // Likewise
+ private Locale mLocale;
+ // Cache this for performance
+ private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
+ private final AndroidSpellCheckerService mService;
+ protected final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
+
+ private static class SuggestionsParams {
+ public final String[] mSuggestions;
+ public final int mFlags;
+ public SuggestionsParams(String[] suggestions, int flags) {
+ mSuggestions = suggestions;
+ mFlags = flags;
+ }
+ }
+
+ protected static class SuggestionsCache {
+ private static final char CHAR_DELIMITER = '\uFFFC';
+ private static final int MAX_CACHE_SIZE = 50;
+ private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
+ new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
+
+ // TODO: Support n-gram input
+ private static String generateKey(String query, String prevWord) {
+ if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) {
+ return query;
+ }
+ return query + CHAR_DELIMITER + prevWord;
+ }
+
+ // TODO: Support n-gram input
+ public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) {
+ return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord));
+ }
+
+ // TODO: Support n-gram input
+ public void putSuggestionsToCache(
+ String query, String prevWord, String[] suggestions, int flags) {
+ if (suggestions == null || TextUtils.isEmpty(query)) {
+ return;
+ }
+ mUnigramSuggestionsInfoCache.put(
+ generateKey(query, prevWord), new SuggestionsParams(suggestions, flags));
+ }
+ }
+
+ AndroidWordLevelSpellCheckerSession(final AndroidSpellCheckerService service) {
+ mService = service;
+ }
+
+ @Override
+ public void onCreate() {
+ final String localeString = getLocale();
+ mDictionaryPool = mService.getDictionaryPool(localeString);
+ mLocale = LocaleUtils.constructLocaleFromString(localeString);
+ mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale);
+ }
+
+ /*
+ * Returns whether the code point is a letter that makes sense for the specified
+ * locale for this spell checker.
+ * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
+ * and is limited to EFIGS languages and Russian.
+ * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
+ * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
+ */
+ private static boolean isLetterCheckableByLanguage(final int codePoint,
+ final int script) {
+ switch (script) {
+ case AndroidSpellCheckerService.SCRIPT_LATIN:
+ // Our supported latin script dictionaries (EFIGS) at the moment only include
+ // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
+ // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
+ // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
+ // excluded from isLetter anyway.
+ return codePoint <= 0x2AF && Character.isLetter(codePoint);
+ case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+ // All Cyrillic characters are in the 400~52F block. There are some in the upper
+ // Unicode range, but they are archaic characters that are not used in modern
+ // russian and are not used by our dictionary.
+ return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
+ default:
+ // Should never come here
+ throw new RuntimeException("Impossible value of script: " + script);
+ }
+ }
+
+ /**
+ * Finds out whether a particular string should be filtered out of spell checking.
+ *
+ * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
+ * we know we will never recognize, this accepts a script identifier that should be one
+ * of the SCRIPT_* constants defined above, to rule out quickly characters from very
+ * different languages.
+ *
+ * @param text the string to evaluate.
+ * @param script the identifier for the script this spell checker recognizes
+ * @return true if we should filter this text out, false otherwise
+ */
+ private static boolean shouldFilterOut(final String text, final int script) {
+ if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
+
+ // TODO: check if an equivalent processing can't be done more quickly with a
+ // compiled regexp.
+ // Filter by first letter
+ final int firstCodePoint = text.codePointAt(0);
+ // Filter out words that don't start with a letter or an apostrophe
+ if (!isLetterCheckableByLanguage(firstCodePoint, script)
+ && '\'' != firstCodePoint) return true;
+
+ // Filter contents
+ final int length = text.length();
+ int letterCount = 0;
+ for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ // Any word containing a '@' is probably an e-mail address
+ // Any word containing a '/' is probably either an ad-hoc combination of two
+ // words or a URI - in either case we don't want to spell check that
+ if ('@' == codePoint || '/' == codePoint) return true;
+ if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
+ }
+ // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
+ // in this word are letters
+ return (letterCount * 4 < length * 3);
+ }
+
+ // Note : this must be reentrant
+ /**
+ * Gets a list of suggestions for a specific string. This returns a list of possible
+ * corrections for the text passed as an argument. It may split or group words, and
+ * even perform grammatical analysis.
+ */
+ @Override
+ public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
+ final int suggestionsLimit) {
+ return onGetSuggestions(textInfo, null, suggestionsLimit);
+ }
+
+ protected SuggestionsInfo onGetSuggestions(
+ final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
+ try {
+ final String inText = textInfo.getText();
+ final SuggestionsParams cachedSuggestionsParams =
+ mSuggestionsCache.getSuggestionsFromCache(inText, prevWord);
+ if (cachedSuggestionsParams != null) {
+ if (DBG) {
+ Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
+ }
+ return new SuggestionsInfo(
+ cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
+ }
+
+ if (shouldFilterOut(inText, mScript)) {
+ DictAndProximity dictInfo = null;
+ try {
+ dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+ if (!DictionaryPool.isAValidDictionary(dictInfo)) {
+ return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ }
+ return dictInfo.mDictionary.isValidWord(inText)
+ ? AndroidSpellCheckerService.getInDictEmptySuggestions()
+ : AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ } finally {
+ if (null != dictInfo) {
+ if (!mDictionaryPool.offer(dictInfo)) {
+ Log.e(TAG, "Can't re-insert a dictionary into its pool");
+ }
+ }
+ }
+ }
+ final String text = inText.replaceAll(
+ AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE);
+
+ // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
+ //final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
+ //mService.mSuggestionThreshold, mService.mRecommendedThreshold,
+ //suggestionsLimit);
+ final SuggestionsGatherer suggestionsGatherer = mService.newSuggestionsGatherer(
+ text, suggestionsLimit);
+ final WordComposer composer = new WordComposer();
+ final int length = text.length();
+ for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ // The getXYForCodePointAndScript method returns (Y << 16) + X
+ final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
+ codePoint, mScript);
+ if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
+ composer.add(codePoint,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ } else {
+ composer.add(codePoint, xy & 0xFFFF, xy >> 16);
+ }
+ }
+
+ final int capitalizeType = AndroidSpellCheckerService.getCapitalizationType(text);
+ boolean isInDict = true;
+ DictAndProximity dictInfo = null;
+ try {
+ dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+ if (!DictionaryPool.isAValidDictionary(dictInfo)) {
+ return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ }
+ final ArrayList<SuggestedWordInfo> suggestions =
+ dictInfo.mDictionary.getSuggestions(composer, prevWord,
+ dictInfo.mProximityInfo);
+ for (final SuggestedWordInfo suggestion : suggestions) {
+ final String suggestionStr = suggestion.mWord.toString();
+ suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
+ suggestionStr.length(), suggestion.mScore);
+ }
+ isInDict = dictInfo.mDictionary.isValidWord(text);
+ if (!isInDict && AndroidSpellCheckerService.CAPITALIZE_NONE != capitalizeType) {
+ // We want to test the word again if it's all caps or first caps only.
+ // If it's fully down, we already tested it, if it's mixed case, we don't
+ // want to test a lowercase version of it.
+ isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
+ }
+ } finally {
+ if (null != dictInfo) {
+ if (!mDictionaryPool.offer(dictInfo)) {
+ Log.e(TAG, "Can't re-insert a dictionary into its pool");
+ }
+ }
+ }
+
+ final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
+ capitalizeType, mLocale);
+
+ if (DBG) {
+ Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
+ + suggestionsLimit);
+ Log.i(TAG, "IsInDict = " + isInDict);
+ Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
+ Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
+ if (null != result.mSuggestions) {
+ for (String suggestion : result.mSuggestions) {
+ Log.i(TAG, suggestion);
+ }
+ }
+ }
+
+ final int flags =
+ (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
+ : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
+ | (result.mHasRecommendedSuggestions
+ ? SuggestionsInfoCompatUtils
+ .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
+ : 0);
+ final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
+ mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags);
+ return retval;
+ } catch (RuntimeException e) {
+ // Don't kill the keyboard if there is a bug in the spell checker
+ if (DBG) {
+ throw e;
+ } else {
+ Log.e(TAG, "Exception while spellcheking: " + e);
+ return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index 8fc632ee7..53aa6c719 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -16,19 +16,56 @@
package com.android.inputmethod.latin.spellcheck;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.WordComposer;
+
+import java.util.ArrayList;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
/**
* A blocking queue that creates dictionaries up to a certain limit as necessary.
+ * As a deadlock-detecting device, if waiting for more than TIMEOUT = 3 seconds, we
+ * will clear the queue and generate its contents again. This is transparent for
+ * the client code, but may help with sloppy clients.
*/
@SuppressWarnings("serial")
public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
+ private final static String TAG = DictionaryPool.class.getSimpleName();
+ // How many seconds we wait for a dictionary to become available. Past this delay, we give up in
+ // fear some bug caused a deadlock, and reset the whole pool.
+ private final static int TIMEOUT = 3;
private final AndroidSpellCheckerService mService;
private final int mMaxSize;
private final Locale mLocale;
private int mSize;
private volatile boolean mClosed;
+ final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList();
+ private final static DictAndProximity dummyDict = new DictAndProximity(
+ new Dictionary(Dictionary.TYPE_MAIN) {
+ @Override
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ return noSuggestions;
+ }
+ @Override
+ public boolean isValidWord(CharSequence word) {
+ // This is never called. However if for some strange reason it ever gets
+ // called, returning true is less destructive (it will not underline the
+ // word in red).
+ return true;
+ }
+ }, null);
+
+ static public boolean isAValidDictionary(final DictAndProximity dictInfo) {
+ return null != dictInfo && dummyDict != dictInfo;
+ }
public DictionaryPool(final int maxSize, final AndroidSpellCheckerService service,
final Locale locale) {
@@ -41,13 +78,23 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
}
@Override
- public DictAndProximity take() throws InterruptedException {
+ public DictAndProximity poll(final long timeout, final TimeUnit unit)
+ throws InterruptedException {
final DictAndProximity dict = poll();
if (null != dict) return dict;
synchronized(this) {
if (mSize >= mMaxSize) {
- // Our pool is already full. Wait until some dictionary is ready.
- return super.take();
+ // Our pool is already full. Wait until some dictionary is ready, or TIMEOUT
+ // expires to avoid a deadlock.
+ final DictAndProximity result = super.poll(timeout, unit);
+ if (null == result) {
+ Log.e(TAG, "Deadlock detected ! Resetting dictionary pool");
+ clear();
+ mSize = 1;
+ return mService.createDictAndProximity(mLocale);
+ } else {
+ return result;
+ }
} else {
++mSize;
return mService.createDictAndProximity(mLocale);
@@ -56,9 +103,9 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
}
// Convenience method
- public DictAndProximity takeOrGetNull() {
+ public DictAndProximity pollWithDefaultTimeout() {
try {
- return take();
+ return poll(TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return null;
}
@@ -78,7 +125,7 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
public boolean offer(final DictAndProximity dict) {
if (mClosed) {
dict.mDictionary.close();
- return false;
+ return super.offer(dummyDict);
} else {
return super.offer(dict);
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
index 0103e8423..fe5225ebd 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
@@ -16,14 +16,15 @@
package com.android.inputmethod.latin.spellcheck;
-import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
import java.util.TreeMap;
public class SpellCheckerProximityInfo {
/* public for test */
- final public static int NUL = KeyDetector.NOT_A_CODE;
+ final public static int NUL = Constants.NOT_A_CODE;
// This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside
// native code - this value is passed at creation of the binary object and reused
@@ -59,7 +60,7 @@ public class SpellCheckerProximityInfo {
// character.
// Since we need to build such an array, we want to be able to search in our big proximity
// data quickly by character, and a map is probably the best way to do this.
- final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+ final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap();
// The proximity here is the union of
// - the proximity for a QWERTY keyboard.
@@ -111,6 +112,7 @@ public class SpellCheckerProximityInfo {
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
};
static {
buildProximityIndices(PROXIMITY, INDICES);
@@ -121,7 +123,7 @@ public class SpellCheckerProximityInfo {
}
private static class Cyrillic {
- final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+ final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap();
// TODO: The following table is solely based on the keyboard layout. Consult with Russian
// speakers on commonly misspelled words/letters.
final static int[] PROXIMITY = {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index c6fe43b69..58b01aa55 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -42,10 +42,10 @@ public class MoreSuggestions extends Keyboard {
private int mToPos;
public static class MoreSuggestionsParam extends Keyboard.Params {
- private final int[] mWidths = new int[SuggestionsView.MAX_SUGGESTIONS];
- private final int[] mRowNumbers = new int[SuggestionsView.MAX_SUGGESTIONS];
- private final int[] mColumnOrders = new int[SuggestionsView.MAX_SUGGESTIONS];
- private final int[] mNumColumnsInRow = new int[SuggestionsView.MAX_SUGGESTIONS];
+ private final int[] mWidths = new int[SuggestionStripView.MAX_SUGGESTIONS];
+ private final int[] mRowNumbers = new int[SuggestionStripView.MAX_SUGGESTIONS];
+ private final int[] mColumnOrders = new int[SuggestionStripView.MAX_SUGGESTIONS];
+ private final int[] mNumColumnsInRow = new int[SuggestionStripView.MAX_SUGGESTIONS];
private static final int MAX_COLUMNS_IN_ROW = 3;
private int mNumRows;
public Drawable mDivider;
@@ -63,7 +63,7 @@ public class MoreSuggestions extends Keyboard {
int row = 0;
int pos = fromPos, rowStartPos = fromPos;
- final int size = Math.min(suggestions.size(), SuggestionsView.MAX_SUGGESTIONS);
+ final int size = Math.min(suggestions.size(), SuggestionStripView.MAX_SUGGESTIONS);
while (pos < size) {
final String word = suggestions.getWord(pos).toString();
// TODO: Should take care of text x-scaling.
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index 19287e3f3..5b23d7f3c 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -68,7 +68,7 @@ public class MoreSuggestionsView extends KeyboardView implements MoreKeysPanel {
@Override
public void onCodeInput(int primaryCode, int x, int y) {
final int index = primaryCode - MoreSuggestions.SUGGESTION_CODE_BASE;
- if (index >= 0 && index < SuggestionsView.MAX_SUGGESTIONS) {
+ if (index >= 0 && index < SuggestionStripView.MAX_SUGGESTIONS) {
mListener.onCustomRequest(index);
}
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index e86390b11..afc4293c0 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -57,22 +57,23 @@ import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.MoreKeysPanel;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.keyboard.ViewLayoutUtils;
+import com.android.inputmethod.latin.AutoCorrection;
+import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResearchLogger;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
-import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
import java.util.ArrayList;
-public class SuggestionsView extends RelativeLayout implements OnClickListener,
+public class SuggestionStripView extends RelativeLayout implements OnClickListener,
OnLongClickListener {
public interface Listener {
- public boolean addWordToDictionary(String word);
- public void pickSuggestionManually(int index, CharSequence word, int x, int y);
+ public boolean addWordToUserDictionary(String word);
+ public void pickSuggestionManually(int index, CharSequence word);
}
// The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
@@ -88,9 +89,9 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
private final PopupWindow mMoreSuggestionsWindow;
- private final ArrayList<TextView> mWords = new ArrayList<TextView>();
- private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
- private final ArrayList<View> mDividers = new ArrayList<View>();
+ private final ArrayList<TextView> mWords = CollectionUtils.newArrayList();
+ private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList();
+ private final ArrayList<View> mDividers = CollectionUtils.newArrayList();
private final PopupWindow mPreviewPopup;
private final TextView mPreviewText;
@@ -98,24 +99,24 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private Listener mListener;
private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
- private final SuggestionsViewParams mParams;
+ private final SuggestionStripViewParams mParams;
private static final float MIN_TEXT_XSCALE = 0.70f;
private final UiHandler mHandler = new UiHandler(this);
- private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionsView> {
+ private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionStripView> {
private static final int MSG_HIDE_PREVIEW = 0;
- public UiHandler(SuggestionsView outerInstance) {
+ public UiHandler(SuggestionStripView outerInstance) {
super(outerInstance);
}
@Override
public void dispatchMessage(Message msg) {
- final SuggestionsView suggestionsView = getOuterInstance();
+ final SuggestionStripView suggestionStripView = getOuterInstance();
switch (msg.what) {
case MSG_HIDE_PREVIEW:
- suggestionsView.hidePreview();
+ suggestionStripView.hidePreview();
break;
}
}
@@ -129,7 +130,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
}
}
- private static class SuggestionsViewParams {
+ private static class SuggestionStripViewParams {
private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
private static final int DEFAULT_CENTER_SUGGESTION_PERCENTILE = 40;
private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
@@ -167,7 +168,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private final int mSuggestionStripOption;
- private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
+ private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList();
public boolean mMoreSuggestionsAvailable;
@@ -175,7 +176,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private final TextView mLeftwardsArrowView;
private final TextView mHintToSaveView;
- public SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle,
+ public SuggestionStripViewParams(Context context, AttributeSet attrs, int defStyle,
ArrayList<TextView> words, ArrayList<View> dividers, ArrayList<TextView> infos) {
mWords = words;
mDividers = dividers;
@@ -191,38 +192,39 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
final Resources res = word.getResources();
mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
- final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.SuggestionsView, defStyle, R.style.SuggestionsViewStyle);
- mSuggestionStripOption = a.getInt(R.styleable.SuggestionsView_suggestionStripOption, 0);
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
+ mSuggestionStripOption = a.getInt(
+ R.styleable.SuggestionStripView_suggestionStripOption, 0);
final float alphaValidTypedWord = getPercent(a,
- R.styleable.SuggestionsView_alphaValidTypedWord, 100);
+ R.styleable.SuggestionStripView_alphaValidTypedWord, 100);
final float alphaTypedWord = getPercent(a,
- R.styleable.SuggestionsView_alphaTypedWord, 100);
+ R.styleable.SuggestionStripView_alphaTypedWord, 100);
final float alphaAutoCorrect = getPercent(a,
- R.styleable.SuggestionsView_alphaAutoCorrect, 100);
+ R.styleable.SuggestionStripView_alphaAutoCorrect, 100);
final float alphaSuggested = getPercent(a,
- R.styleable.SuggestionsView_alphaSuggested, 100);
- mAlphaObsoleted = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100);
- mColorValidTypedWord = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorValidTypedWord, 0),
- alphaValidTypedWord);
- mColorTypedWord = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorTypedWord, 0), alphaTypedWord);
- mColorAutoCorrect = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorAutoCorrect, 0), alphaAutoCorrect);
- mColorSuggested = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorSuggested, 0), alphaSuggested);
+ R.styleable.SuggestionStripView_alphaSuggested, 100);
+ mAlphaObsoleted = getPercent(a,
+ R.styleable.SuggestionStripView_alphaSuggested, 100);
+ mColorValidTypedWord = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
+ mColorTypedWord = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
+ mColorAutoCorrect = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
+ mColorSuggested = applyAlpha(a.getColor(
+ R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
mSuggestionsCountInStrip = a.getInt(
- R.styleable.SuggestionsView_suggestionsCountInStrip,
+ R.styleable.SuggestionStripView_suggestionsCountInStrip,
DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
mCenterSuggestionWeight = getPercent(a,
- R.styleable.SuggestionsView_centerSuggestionPercentile,
+ R.styleable.SuggestionStripView_centerSuggestionPercentile,
DEFAULT_CENTER_SUGGESTION_PERCENTILE);
mMaxMoreSuggestionsRow = a.getInt(
- R.styleable.SuggestionsView_maxMoreSuggestionsRow,
+ R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
mMinMoreSuggestionsWidth = getRatio(a,
- R.styleable.SuggestionsView_minMoreSuggestionsWidth);
+ R.styleable.SuggestionStripView_minMoreSuggestionsWidth);
a.recycle();
mMoreSuggestionsHint = getMoreSuggestionsHint(res,
@@ -336,8 +338,8 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
// If we auto-correct, then the autocorrection is in slot 0 and the typed word
// is in slot 1.
- if (index == mCenterSuggestionIndex && suggestedWords.mHasAutoCorrectionCandidate
- && Suggest.shouldBlockAutoCorrectionBySafetyNet(
+ if (index == mCenterSuggestionIndex
+ && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
suggestedWords.getWord(1).toString(), suggestedWords.getWord(0))) {
return 0xFFFF0000;
}
@@ -596,15 +598,15 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
}
/**
- * Construct a {@link SuggestionsView} for showing suggestions to be picked by the user.
+ * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
* @param context
* @param attrs
*/
- public SuggestionsView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.suggestionsViewStyle);
+ public SuggestionStripView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.suggestionStripViewStyle);
}
- public SuggestionsView(Context context, AttributeSet attrs, int defStyle) {
+ public SuggestionStripView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final LayoutInflater inflater = LayoutInflater.from(context);
@@ -631,7 +633,8 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
}
- mParams = new SuggestionsViewParams(context, attrs, defStyle, mWords, mDividers, mInfos);
+ mParams = new SuggestionStripViewParams(
+ context, attrs, defStyle, mWords, mDividers, mInfos);
mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
@@ -677,7 +680,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
mSuggestedWords = suggestedWords;
mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth());
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.suggestionsView_setSuggestions(mSuggestedWords);
+ ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
}
}
@@ -718,19 +721,13 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
mPreviewPopup.dismiss();
}
- private void addToDictionary(CharSequence word) {
- mListener.addWordToDictionary(word.toString());
- }
-
private final KeyboardActionListener mMoreSuggestionsListener =
new KeyboardActionListener.Adapter() {
@Override
public boolean onCustomRequest(int requestCode) {
final int index = requestCode;
final CharSequence word = mSuggestedWords.getWord(index);
- // TODO: change caller path so coordinates are passed through here
- mListener.pickSuggestionManually(index, word, NOT_A_TOUCH_COORDINATE,
- NOT_A_TOUCH_COORDINATE);
+ mListener.pickSuggestionManually(index, word);
dismissMoreSuggestions();
return true;
}
@@ -763,7 +760,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
}
private boolean showMoreSuggestions() {
- final SuggestionsViewParams params = mParams;
+ final SuggestionStripViewParams params = mParams;
if (params.mMoreSuggestionsAvailable) {
final int stripWidth = getWidth();
final View container = mMoreSuggestionsContainer;
@@ -863,7 +860,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
@Override
public void onClick(View view) {
if (mParams.isAddToDictionaryShowing(view)) {
- addToDictionary(mParams.getAddToDictionaryWord());
+ mListener.addWordToUserDictionary(mParams.getAddToDictionaryWord().toString());
clear();
return;
}
@@ -876,7 +873,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
return;
final CharSequence word = mSuggestedWords.getWord(index);
- mListener.pickSuggestionManually(index, word, mLastX, mLastY);
+ mListener.pickSuggestionManually(index, word);
}
@Override